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.
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
binzawiera twój program - Skompilujesz i uruchomisz projekt z wiersza poleceń
- Odczytasz komunikat o błędzie kompilacji i poprawisz kod
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.
Dwa etapy zamiast jednego
W C# tłumaczenie nie odbywa się jednym skokiem. Dzieje się w dwóch krokach, w różnych momentach.
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.
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.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
| Plik | Do czego służy |
|---|---|
.dll | Zestaw (ang. assembly) — zawiera twój kod przetłumaczony na IL oraz opis wszystkich typów. To jest właściwy efekt kompilacji. |
.exe | Program uruchamiający. Nie ma go, gdy budujesz bibliotekę zamiast aplikacji. |
.pdb | Mapa łą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.json | Zwykły plik tekstowy — możesz go otworzyć w Notatniku. Mówi, jakiej wersji środowiska .NET wymaga program. |
Debug czy Release?
| Konfiguracja | Kiedy | Różnica |
|---|---|---|
| Debug | podczas 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. |
| Release | gdy 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.
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ą.
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
| Polecenie | Co robi |
|---|---|
dotnet new console | Tworzy nowy projekt konsolowy: plik Program.cs i plik projektu .csproj. Przełącznik -o podaje nazwę folderu. |
dotnet build | Uruchamia kompilator Roslyn. Wynik ląduje w bin\Debug\. Program się nie uruchamia. |
dotnet run | Buduje, jeśli trzeba, i od razu uruchamia program. To odpowiednik klawisza F5. |
dotnet clean | Kasuje 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:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
| Wpis | Znaczenie |
|---|---|
OutputType | Exe — program do uruchomienia. Library — biblioteka, z której korzystają inne projekty. |
TargetFramework | Wersja .NET, pod którą budujemy. net10.0 oznacza .NET 10. |
ImplicitUsings | Automatyczne dołączanie najpopularniejszych przestrzeni nazw — dlatego Console działa bez using System;. |
Nullable | Włącza ostrzeżenia o zmiennych, które mogą nie zawierać żadnej wartości. Wrócimy do tego w bloku o C# nowoczesnym. |
Jak czytać komunikaty kompilatora
Komunikat kompilatora ma zawsze ten sam układ. Warto raz nauczyć się go czytać — potem oszczędzi ci to godziny.
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?
| Rodzaj | Co 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
| Kod | Komunikat | Zwykła przyczyna |
|---|---|---|
CS1002 | Oczekiwano ; | Brak średnika na końcu instrukcji. |
CS0103 | Nazwa x nie istnieje w bieżącym kontekście | Literówka w nazwie zmiennej albo użycie jej przed zadeklarowaniem. Pamiętaj: wielkość liter ma znaczenie. |
CS1061 | Typ nie zawiera definicji x | Literówka w nazwie metody — np. Writeline zamiast WriteLine. |
CS1525 | Nieprawidłowy token | Zwykle niedomknięty nawias albo cudzysłów. |
CS1513 | Oczekiwano } | Brak klamry zamykającej. Skrót Ctrl + K, Ctrl + D formatuje dokument — po nim źle domknięte klamry rzucają się w oczy. |
CS5001 | Program nie zawiera statycznej metody Main | Projekt jest typu Exe, ale nie ma punktu wejścia. O tym w następnej lekcji. |
Zadania
Poniższy program się nie kompiluje. Przepisz go do Visual Studio, odczytaj komunikaty i popraw wszystkie błędy. Jest ich cztery.
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 3 — Writeline 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.
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
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.
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
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.
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.
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
binzawiera 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).
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.exejest 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.