Napisałeś tekst w edytorze, nacisnąłeś klawisz i coś się uruchomiło. Ale procesor nie rozumie słowa Console — rozumie tylko liczby. Ta lekcja pokazuje, co dzieje się po drodze i dlaczego dzięki temu ten sam program działa na Windowsie i na Linuksie.

C# kompilator IL JIT 40 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, czym różni się kompilator od interpretera
  • Opiszesz dwa etapy, przez które przechodzi kod C#, zanim się wykona
  • Wyjaśnisz, po co istnieje kod pośredni IL i co robi kompilator JIT
  • Rozpoznasz, który plik w folderze bin zawiera twój program
  • Skompilujesz i uruchomisz projekt z wiersza poleceń
  • Odczytasz komunikat o błędzie kompilacji i poprawisz kod
TEORIA

Problem: procesor nie zna C#

Kod, który piszesz, jest zrozumiały dla człowieka. Procesor rozumie coś zupełnie innego — kod maszynowy, czyli ciąg liczb oznaczających elementarne operacje: „weź liczbę z pamięci”, „dodaj”, „skocz do innego miejsca”. Ktoś musi jedno przetłumaczyć na drugie. Robi to kompilator.

Kompilator a interpreter

To dwa różne podejścia do tego samego zadania.

Kompilator

Tłumaczy cały program naraz, przed uruchomieniem. Powstaje osobny plik, który potem uruchamiasz. Błędy wychodzą podczas tłumaczenia — program w ogóle nie powstanie, dopóki ich nie poprawisz.

Interpreter

Czyta i wykonuje program linia po linii, w trakcie działania. Nie powstaje żaden plik wynikowy. Błąd w dziesiątej linii wyjdzie dopiero wtedy, gdy program do niej dojdzie.

Analogia: tłumacz książki a tłumacz na konferencji

Kompilator jest jak tłumacz książki: dostaje cały tekst, tłumaczy go w całości i oddaje gotowy przekład. Jeśli w oryginale jest zdanie bez sensu, zauważy to podczas pracy.

Interpreter jest jak tłumacz na żywo: tłumaczy zdanie po zdaniu, w miarę jak mówca je wypowiada. Nie wie z góry, co padnie za pięć minut, więc o błędzie dowie się dopiero, gdy do niego dojdzie.

Języki takie jak Python czy JavaScript są tradycyjnie interpretowane. C# jest kompilowany — i to właśnie dlatego Visual Studio potrafi podkreślić błąd na czerwono, zanim jeszcze uruchomisz program.

TEORIA

Dwa etapy zamiast jednego

W C# tłumaczenie nie odbywa się jednym skokiem. Dzieje się w dwóch krokach, w różnych momentach.

droga twojego kodu
  Program.cs                    plik .dll                     kod maszynowy
  (kod w C#)                    (kod pośredni IL)             (rozumie procesor)
      │                              │                              │
      │   ETAP 1: kompilator         │   ETAP 2: kompilator JIT     │
      │   Roslyn                     │   w środowisku .NET          │
      └─────────────────────────────►└─────────────────────────────►
      gdy klikasz „Zbuduj"           gdy program działa

Etap 1 — kompilator Roslyn: C# → kod pośredni

Gdy budujesz projekt, do pracy zabiera się Roslyn — kompilator języka C#. Sprawdza kod pod względem poprawności i tłumaczy go na kod pośredni, nazywany IL (ang. Intermediate Language).

IL to język pośredni między C# a kodem maszynowym. Nie jest przeznaczony dla człowieka, ale też nie jest jeszcze zrozumiały dla procesora. Ważne: IL nie zależy od tego, na jakim procesorze i systemie ostatecznie uruchomisz program.

Etap 2 — kompilator JIT: kod pośredni → kod maszynowy

Gdy uruchamiasz program, wkracza środowisko uruchomieniowe .NET. Zawiera ono kompilator JIT (ang. Just-In-Time — „w samą porę”), który tłumaczy IL na kod maszynowy dopiero w trakcie działania programu i dokładnie dla tego procesora, na którym program właśnie działa.

Po co ta komplikacja? Dla przenośności

Gdyby C# kompilował się od razu do kodu maszynowego, trzeba by tworzyć osobną wersję programu dla Windowsa, osobną dla Linuksa, osobną dla procesorów ARM. Dzięki kodowi pośredniemu powstaje jedna wersja, a dopasowaniem do konkretnego sprzętu zajmuje się JIT już na miejscu.

Ta sama idea działa w Javie — tam kod pośredni nazywa się bytecode, a środowisko to maszyna wirtualna JVM.

Czy JIT nie spowalnia programu?

Trochę tak — przy pierwszym wywołaniu metody JIT musi ją najpierw przetłumaczyć. Ale robi to tylko raz: wynik zostaje w pamięci i kolejne wywołania idą już z pełną prędkością. Co więcej, JIT wie, na jakim procesorze działa, więc może wykorzystać jego konkretne możliwości — czego kompilator na twoim komputerze nie mógłby przewidzieć.

Dlaczego C# nazywa się językiem hybrydowym

Bo łączy oba podejścia: kompiluje się jak język kompilowany (błędy wychodzą przed uruchomieniem) i tłumaczy się w trakcie działania jak język interpretowany (dopasowanie do konkretnego sprzętu). Stąd też wzięło się określenie „zarządzany” (ang. managed) — twój kod nie działa bezpośrednio na procesorze, tylko pod opieką środowiska .NET, które pilnuje pamięci i bezpieczeństwa typów.

PRZYKŁAD Z OMÓWIENIEM

Co naprawdę powstaje na dysku

Zbuduj projekt z poprzedniej lekcji i zajrzyj do folderu bin\Debug\. Znajdziesz tam kilka plików — warto wiedzieć, który jest który.

PierwszyProgram\bin\Debug\net10.0\
PierwszyProgram.dll                 ← TUTAJ jest twój kod (w postaci IL)
PierwszyProgram.exe                 ← mały program uruchamiający
PierwszyProgram.pdb                 ← informacje dla debugera
PierwszyProgram.runtimeconfig.json  ← która wersja .NET ma to uruchomić
PierwszyProgram.deps.json           ← lista zależności projektu

To nie plik .exe zawiera twój program

Wbrew intuicji twój kod znajduje się w pliku .dll. Plik .exe to tylko kilkadziesiąt kilobajtów programu uruchamiającego (ang. apphost), którego jedynym zadaniem jest odnalezienie środowiska .NET i podanie mu pliku .dll do wykonania.

Na Linuksie i macOS nie ma żadnego .exe — jest plik bez rozszerzenia, pełniący tę samą rolę. Plik .dll jest natomiast identyczny na każdym systemie.

Omówienie

PlikDo czego służy
.dllZestaw (ang. assembly) — zawiera twój kod przetłumaczony na IL oraz opis wszystkich typów. To jest właściwy efekt kompilacji.
.exeProgram uruchamiający. Nie ma go, gdy budujesz bibliotekę zamiast aplikacji.
.pdbMapa łącząca IL z konkretnymi liniami twojego pliku .cs. Dzięki niej debuger wie, przy której linii się zatrzymać, a komunikat o błędzie potrafi podać numer wiersza.
.runtimeconfig.jsonZwykły plik tekstowy — możesz go otworzyć w Notatniku. Mówi, jakiej wersji środowiska .NET wymaga program.

Debug czy Release?

KonfiguracjaKiedyRóżnica
Debugpodczas pisania Kod nie jest optymalizowany, za to zawiera pełne informacje dla debugera. Program działa wolniej, ale można go zatrzymać w dowolnym miejscu i podejrzeć zmienne.
Releasegdy program jest gotowy Kompilator optymalizuje kod — usuwa zbędne operacje, upraszcza obliczenia. Program jest szybszy, ale debugowanie staje się utrudnione.

Konfigurację zmienia się na pasku narzędzi Visual Studio, w rozwijanym polu obok przycisku uruchamiania. Na czas nauki zostaw Debug — potrzebujesz dokładnych komunikatów o błędach bardziej niż szybkości.

NARZĘDZIA

To samo z wiersza poleceń

Visual Studio wykonuje kompilację za ciebie, ale wszystko to samo można zrobić poleceniem dotnet. Warto je znać: na egzaminie zawodowym bywa szybsze, a na Linuksie często jest jedyną drogą.

wiersz poleceń
dotnet new console -o MojProgram   # utwórz nowy projekt w folderze MojProgram
cd MojProgram                      # wejdź do tego folderu
dotnet build                       # skompiluj (C# → IL)
dotnet run                         # skompiluj i od razu uruchom
dotnet clean                       # usuń wyniki kompilacji
PolecenieCo robi
dotnet new consoleTworzy nowy projekt konsolowy: plik Program.cs i plik projektu .csproj. Przełącznik -o podaje nazwę folderu.
dotnet buildUruchamia kompilator Roslyn. Wynik ląduje w bin\Debug\. Program się nie uruchamia.
dotnet runBuduje, jeśli trzeba, i od razu uruchamia program. To odpowiednik klawisza F5.
dotnet cleanKasuje zawartość folderów bin i obj. Przydatne, gdy projekt zachowuje się dziwnie po wielu zmianach.

Plik projektu

Obok Program.cs leży plik z rozszerzeniem .csproj. To zwykły plik XML z ustawieniami projektu:

MojProgram.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
WpisZnaczenie
OutputTypeExe — program do uruchomienia. Library — biblioteka, z której korzystają inne projekty.
TargetFrameworkWersja .NET, pod którą budujemy. net10.0 oznacza .NET 10.
ImplicitUsingsAutomatyczne dołączanie najpopularniejszych przestrzeni nazw — dlatego Console działa bez using System;.
NullableWłącza ostrzeżenia o zmiennych, które mogą nie zawierać żadnej wartości. Wrócimy do tego w bloku o C# nowoczesnym.
BŁĘDY KOMPILACJI

Jak czytać komunikaty kompilatora

Komunikat kompilatora ma zawsze ten sam układ. Warto raz nauczyć się go czytać — potem oszczędzi ci to godziny.

okno Lista błędów
Program.cs(7,42): error CS1002: Oczekiwano ;
     │        │ │         │
     │        │ │         └── treść: co jest nie tak
     │        │ └── kod błędu — wpisz go w wyszukiwarkę, to działa
     │        └── wiersz 7, znak 42
     └── plik, w którym wystąpił problem

Zawsze poprawiaj od pierwszego błędu

Kompilator, gdy się pogubi, potrafi zgłosić kilkanaście błędów wynikających z jednej literówki. Popraw pierwszy z listy i zbuduj projekt ponownie — bardzo często reszta zniknie sama. Poprawianie od dołu to strata czasu.

Błąd czy ostrzeżenie?

RodzajCo oznacza
error (czerwony)Kompilator nie potrafi zbudować programu. Musisz to poprawić — nic się nie uruchomi.
warning (żółty)Kod się skompiluje i uruchomi, ale kompilator podejrzewa, że coś jest nie tak. Nie ignoruj ostrzeżeń — bardzo często to zapowiedź błędu, który wyjdzie dopiero u użytkownika.

Najczęstsze błędy na początku

KodKomunikatZwykła przyczyna
CS1002Oczekiwano ;Brak średnika na końcu instrukcji.
CS0103Nazwa x nie istnieje w bieżącym kontekścieLiterówka w nazwie zmiennej albo użycie jej przed zadeklarowaniem. Pamiętaj: wielkość liter ma znaczenie.
CS1061Typ nie zawiera definicji xLiterówka w nazwie metody — np. Writeline zamiast WriteLine.
CS1525Nieprawidłowy tokenZwykle niedomknięty nawias albo cudzysłów.
CS1513Oczekiwano }Brak klamry zamykającej. Skrót Ctrl + K, Ctrl + D formatuje dokument — po nim źle domknięte klamry rzucają się w oczy.
CS5001Program nie zawiera statycznej metody MainProjekt jest typu Exe, ale nie ma punktu wejścia. O tym w następnej lekcji.
ZADANIA

Zadania

ZAD 1Znajdź i popraw cztery błędy★☆☆

Poniższy program się nie kompiluje. Przepisz go do Visual Studio, odczytaj komunikaty i popraw wszystkie błędy. Jest ich cztery.

do poprawienia
using System

namespace MojaAplikacja
{
    class Program
    {
        static void Main(string[] args)
        {
            string imie = "Jan"
            Console.Writeline("Cześć " + imie)
        }
    }
pokaż rozwiązanie
using System;                                  // 1. dodany średnik

namespace MojaAplikacja
{
    class Program
    {
        static void Main(string[] args)
        {
            string imie = "Jan";                      // 2. dodany średnik
            Console.WriteLine("Cześć " + imie);       // 3. WriteLine + średnik
        }
    }
}                                                  // 4. dodana klamra zamykająca

Błąd 1 i 2 — brak średników. Kompilator zgłosi dwa razy CS1002.

Błąd 3Writeline zamiast WriteLine, czyli CS1061. C# rozróżnia wielkość liter, więc dla kompilatora to zupełnie inna nazwa. Do tego brakuje średnika.

Błąd 4 — brak klamry zamykającej przestrzeń nazw, czyli CS1513. Otwartych klamer jest cztery, zamkniętych trzy.

Zwróć uwagę, że kompilator wypisze więcej niż cztery komunikaty — brak ostatniej klamry potrafi wygenerować ich kilka. Dlatego poprawia się od pierwszego.

ZAD 2Kompilacja z wiersza poleceń★★☆

Bez używania Visual Studio: utwórz projekt konsolowy poleceniem dotnet, zmień treść programu w Notatniku, skompiluj i uruchom. Zanotuj, co wypisuje polecenie dotnet build.

pokaż rozwiązanie
wiersz poleceń
dotnet new console -o Test
cd Test
notepad Program.cs        # zmień tekst i zapisz
dotnet build
dotnet run

dotnet build wypisuje ścieżkę do zbudowanego pliku .dll, liczbę ostrzeżeń, liczbę błędów oraz czas kompilacji. Zwróć uwagę, że w komunikacie widnieje .dll, a nie .exe — to potwierdza to, o czym mówiliśmy w sekcji o plikach.

Jeśli dotnet run działa, a dotnet build zgłasza błędy, to znaczy, że uruchamiasz polecenia w niewłaściwym folderze — musisz być w tym, w którym leży plik .csproj.

ZAD 3Debug kontra Release★★☆

Zbuduj ten sam projekt w obu konfiguracjach. Porównaj zawartość folderów bin\Debug\ i bin\Release\ oraz rozmiary powstałych plików. Co się zmieniło?

pokaż rozwiązanie
wiersz poleceń
dotnet build -c Debug
dotnet build -c Release

Oba foldery zawierają te same rodzaje plików. Plik .dll z konfiguracji Release bywa nieco mniejszy, bo kompilator usunął z niego elementy potrzebne wyłącznie do debugowania.

Przy tak małym programie różnica jest ledwie zauważalna — w dużych projektach potrafi być znacząca. Ważniejsze jest to, że plik .pdb z konfiguracji Release zawiera mniej informacji, więc debugowanie takiej wersji jest utrudnione.

ZAD 4Kod pośredni na własne oczy★★★

Pobierz darmowy program ILSpy i otwórz w nim plik .dll swojego projektu. Odszukaj metodę Main i przełącz widok na IL. Porównaj to, co widzisz, ze swoim kodem w C#.

czego szukać

Instrukcja Console.WriteLine("Cześć") zamienia się w IL na mniej więcej dwie operacje: ldstr (wstaw tekst) oraz call (wywołaj metodę).

To dobrze pokazuje, na czym polega kompilacja: jedna czytelna linia w C# staje się kilkoma elementarnymi krokami — a każdy z nich JIT przetłumaczy jeszcze na kilka instrukcji procesora.

Przy okazji zobaczysz, że ILSpy potrafi odtworzyć z pliku .dll kod bardzo podobny do oryginalnego C#. To ważna informacja praktyczna: skompilowany program .NET nie ukrywa kodu źródłowego. Jeśli ktoś chce go chronić, musi użyć osobnego narzędzia zaciemniającego.

ZAD 5Pytania kontrolne★☆☆

Odpowiedz własnymi słowami, nie zaglądając do lekcji:

  • Dlaczego kod C# nie jest tłumaczony od razu na kod maszynowy?
  • Który plik w folderze bin zawiera twój program i dlaczego nie jest to .exe?
  • Kiedy działa kompilator Roslyn, a kiedy kompilator JIT?
  • Czym różni się błąd od ostrzeżenia?
  • Co oznacza, że C# jest językiem hybrydowym?
sprawdź odpowiedzi

1. Żeby ten sam plik działał na różnych systemach i procesorach. Kod pośredni jest niezależny od sprzętu, a dopasowaniem zajmuje się JIT dopiero na komputerze docelowym.

2. Plik .dll. Plik .exe to tylko program uruchamiający, który odnajduje środowisko .NET i podaje mu .dll do wykonania. Na Linuksie .exe w ogóle nie ma.

3. Roslyn — gdy budujesz projekt, czyli zanim program ruszy. JIT — w trakcie działania programu, przy pierwszym wywołaniu każdej metody.

4. Błąd uniemożliwia zbudowanie programu. Ostrzeżenie pozwala go zbudować i uruchomić, ale sygnalizuje coś podejrzanego.

5. Bo łączy kompilację (błędy wykrywane przed uruchomieniem) z tłumaczeniem w trakcie działania (dopasowanie do konkretnego sprzętu).

PODSUMOWANIE

Co trzeba zapamiętać

  • Kompilator tłumaczy cały program przed uruchomieniem, interpreter — linia po linii w trakcie działania.
  • Kod C# przechodzi przez dwa etapy: Roslyn tłumaczy go na kod pośredni IL, a JIT — z IL na kod maszynowy.
  • IL nie zależy od systemu ani procesora — to dzięki niemu jeden plik działa wszędzie.
  • JIT tłumaczy każdą metodę raz, przy jej pierwszym wywołaniu, i dopasowuje kod do procesora, na którym program działa.
  • Twój program mieszka w pliku .dll. Plik .exe jest tylko programem uruchamiającym.
  • Debug — na czas pisania, Release — gdy program jest gotowy.
  • Komunikat błędu podaje plik, wiersz, kod (np. CS1002) i opis. Poprawiaj zawsze od pierwszego błędu.
  • Ostrzeżeń nie wolno ignorować — bardzo często zapowiadają realny problem.