Otwierasz nowy projekt konsolowy w Visual Studio i widzisz cztery linijki zamiast klasy z metodą Main, o której mówią wszystkie podręczniki. Ta lekcja wyjaśnia, skąd wzięły się dwa różne wyglądy pierwszego programu, co się dzieje pod spodem i którego zapisu używać na lekcjach oraz na egzaminie.
Czego się dziś nauczysz
- Rozpoznasz oba zapisy programu konsolowego i wyjaśnisz, czym się różnią
- Wyjaśnisz, czym jest punkt wejścia programu i dlaczego musi być dokładnie jeden
- Przepiszesz program z zapisu skróconego na klasyczny i odwrotnie
- Wskażesz, gdzie w pliku wolno umieścić klasę, a gdzie instrukcję
- Poprawisz błąd CS8803, który dostaje niemal każdy, kto wkleja kod z internetu
Dlaczego pierwszy program wygląda u każdego inaczej
Gdy w Visual Studio utworzysz projekt Console App dla .NET 6 lub nowszego, w pliku Program.cs znajdziesz coś takiego:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
Dwie linijki. Żadnej klasy, żadnego Main, żadnego using System;. Tymczasem w książkach, na starszych kursach i na arkuszach egzaminacyjnych ten sam program wygląda tak:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
Oba programy robią dokładnie to samo i oba są poprawne. Krótszy zapis nazywa się instrukcjami najwyższego poziomu (ang. top-level statements) i pojawił się w C# 9. Nie jest to jednak nowy, „inny” sposób pisania programów — to skrót, który kompilator sam rozwija do postaci klasycznej.
Co robi kompilator z krótkim zapisem
Widząc instrukcje na najwyższym poziomie pliku, kompilator sam opakowuje je w klasę i metodę Main. Twoje dwie linijki stają się mniej więcej tym:
class Program { static void Main(string[] args) { Console.WriteLine("Hello, World!"); } }
Nie widzisz tego w edytorze, ale w skompilowanym programie ta klasa istnieje. Można ją zobaczyć narzędziem takim jak ILSpy.
Skąd znikło using System;
To osobny mechanizm, wprowadzony razem z .NET 6, nazywany niejawnymi dyrektywami using (ang. implicit usings). Projekt konsolowy dopisuje za ciebie kilka najczęściej używanych przestrzeni nazw — między innymi System, System.Collections.Generic, System.Linq i System.IO. Dlatego Console, List czy File działają od razu.
Zajrzyj do pliku projektu .csproj — to tam jest włączane:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Nie wszystko wchodzi automatycznie
ImplicitUsings obejmuje tylko kilkanaście najpopularniejszych przestrzeni nazw. Na przykład System.Globalization (potrzebna przy ustawieniach regionalnych) i System.Text (potrzebna do StringBuilder i kodowania plików) w komplecie nie są — te trzeba dopisać samodzielnie.
Anatomia zapisu klasycznego
Nawet jeśli na co dzień będziesz pisać krótko, musisz rozumieć wersję pełną — spotkasz ją w każdym starszym kodzie, w dokumentacji i na egzaminie zawodowym.
| Element | Co oznacza |
|---|---|
using System; | Udostępnia nazwy z przestrzeni System, dzięki czemu można pisać Console zamiast System.Console. |
namespace HelloWorld | Przestrzeń nazw — logiczny „folder” na klasy. Chroni przed konfliktem, gdy dwie biblioteki mają klasę o tej samej nazwie. |
class Program | Klasa. W C# każdy kod musi znajdować się w jakiejś klasie — nazwa Program to tylko konwencja, można ją zmienić. |
static | Metoda należy do klasy, nie do obiektu. Dzięki temu system może wywołać Main bez tworzenia obiektu klasy Program. |
void | Metoda nic nie zwraca. Można też napisać int — wtedy zwracana liczba staje się kodem wyjścia programu (0 oznacza sukces). |
Main | Punkt wejścia — miejsce, od którego zaczyna się wykonywanie programu. Pisane wielką literą. |
string[] args | Argumenty przekazane programowi z wiersza poleceń. Jeśli ich nie używasz, możesz je pominąć: static void Main(). |
Punkt wejścia musi być dokładnie jeden
Program nie może mieć dwóch metod Main ani jednocześnie Main i instrukcji najwyższego poziomu — kompilator nie wiedziałby, od czego zacząć. To najczęstsza przyczyna błędu przy kopiowaniu kodu z internetu do gotowego projektu.
Który zapis wybrać?
| Sytuacja | Zapis |
|---|---|
| Krótki program ćwiczeniowy, jeden plik | Instrukcje najwyższego poziomu — mniej pisania, mniej rozpraszaczy |
| Program z własnymi klasami | Oba działają, ale pilnuj kolejności (patrz „Częste błędy”) |
| Egzamin zawodowy INF.03 / INF.04 | Klasyczny z Main — jest jednoznaczny i działa w każdej wersji .NET |
| Kod, który ktoś inny będzie czytał | Klasyczny — struktura jest widoczna wprost |
W tym kursie przykłady krótkie zapisujemy skrótowo, a wszędzie tam, gdzie pojawiają się własne klasy, wracamy do zapisu z Main. Każdy listing będzie miał w nagłówku informację, gdzie go wkleić.
Co dokładnie robi każdy użyty element
| Element | Zwraca | Działanie |
|---|---|---|
Console.WriteLine(x) | nic (void) |
Wypisuje x i przechodzi do nowej linii. Console to klasa statyczna z przestrzeni System, więc nie tworzy się jej obiektu. |
Console.Write(x) | nic (void) |
To samo, ale bez przejścia do nowej linii — kursor zostaje na końcu wypisanego tekstu. Używamy tego przed prośbą o dane, żeby użytkownik pisał w tej samej linii. |
Console.ReadLine() | string? |
Czeka, aż użytkownik wpisze tekst i naciśnie Enter, po czym zwraca ten tekst. Znak zapytania w typie oznacza, że metoda może zwrócić null (np. gdy strumień wejścia się skończy) — dlatego przy tej metodzie zobaczysz czasem żółte ostrzeżenie. |
Console.ReadKey() | ConsoleKeyInfo |
Czeka na naciśnięcie dowolnego klawisza. Dawano to na końcu programu, żeby okno konsoli się nie zamknęło. W Visual Studio nie jest już potrzebne — uruchamianie bez debugowania (Ctrl+F5) samo zatrzymuje okno. |
args | string[] |
Tablica argumentów z wiersza poleceń. Po uruchomieniu MojProgram.exe test 42 zawiera dwa elementy: "test" i "42". W zapisie skróconym args jest dostępne mimo braku widocznej deklaracji. |
Ten sam program napisany na dwa sposoby
Program pyta o imię i rok urodzenia, po czym wypisuje powitanie i wiek. Najpierw krótko:
Console.Write("Jak masz na imię? ");
string imie = Console.ReadLine();
Console.Write("W którym roku się urodziłeś? ");
int rok = int.Parse(Console.ReadLine());
int wiek = 2026 - rok;
Console.WriteLine($"Cześć, {imie}! W tym roku kończysz {wiek} lat.");
A teraz dokładnie to samo w zapisie klasycznym:
using System;
namespace Powitanie
{
class Program
{
static void Main(string[] args)
{
Console.Write("Jak masz na imię? ");
string imie = Console.ReadLine();
Console.Write("W którym roku się urodziłeś? ");
int rok = int.Parse(Console.ReadLine());
int wiek = 2026 - rok;
Console.WriteLine($"Cześć, {imie}! W tym roku kończysz {wiek} lat.");
}
}
}
Jak masz na imię? Grzegorz
W którym roku się urodziłeś? 2008
Cześć, Grzegorz! W tym roku kończysz 18 lat.
Omówienie
| Fragment | Dlaczego tak |
|---|---|
Console.Write przy pytaniu |
Bez przejścia do nowej linii, więc użytkownik wpisuje odpowiedź zaraz za znakiem zapytania. Z WriteLine kursor przeskoczyłby niżej. |
int.Parse(...) |
ReadLine zawsze zwraca tekst — nawet gdy użytkownik wpisze cyfry. int.Parse zamienia napis "2008" na liczbę 2008, bez czego odejmowanie byłoby niemożliwe. |
$"…{imie}…" |
Interpolacja tekstu. Znak $ przed cudzysłowem pozwala wstawiać wartości zmiennych bezpośrednio w klamrach — czytelniej niż sklejanie plusami. |
| Ten sam kod w obu wersjach | Zwróć uwagę: treść programu jest identyczna. Różni się wyłącznie obudowa. To dobrze pokazuje, że instrukcje najwyższego poziomu nie zmieniają języka — tylko ukrywają rusztowanie. |
Jeśli zobaczysz zieloną falkę pod Console.ReadLine()
To ostrzeżenie CS8600 — kompilator przypomina, że ReadLine może zwrócić null, a przypisujesz to do zwykłego string. Program zadziała, ostrzeżenie można na razie zignorować. Jak się go pozbyć na dobre, pokażemy w lekcji o typach dopuszczających null.
Na co uważać
1. Klasa przed instrukcjami — najczęstszy błąd przy kopiowaniu kodu
To pułapka, w którą wpada niemal każdy, kto skopiuje przykład ze strony internetowej i wklei go na początek swojego Program.cs:
class Samochod // ✗ najpierw klasa…
{
public string Marka;
}
Samochod auto = new Samochod(); // ✗ CS8803: Top-level statements must precede
auto.Marka = "Toyota"; // namespace and type declarations
Console.WriteLine(auto.Marka);
W pliku z instrukcjami najwyższego poziomu obowiązuje sztywna kolejność: najpierw instrukcje, dopiero potem deklaracje klas. Wystarczy zamienić bloki miejscami:
Samochod auto = new Samochod(); // ✓ najpierw instrukcje
auto.Marka = "Toyota";
Console.WriteLine(auto.Marka);
class Samochod // ✓ klasa na końcu pliku
{
public string Marka;
}
Wygodniejsze rozwiązanie
Klasę możesz też umieścić w osobnym pliku — kliknij projekt prawym przyciskiem → Add → Class, nazwij plik Samochod.cs. Wtedy kolejność w Program.cs przestaje mieć znaczenie, a projekt jest czytelniejszy. Tak robi się w prawdziwych programach.
2. Dwa punkty wejścia naraz
Console.WriteLine("Start"); // instrukcja najwyższego poziomu
class Program
{
static void Main() // ✗ CS8802: Only one compilation unit
{ // can have top-level statements
Console.WriteLine("Main");
}
}
Wybierz jedno: albo instrukcje najwyższego poziomu, albo Main. Nigdy oba w jednym projekcie.
3. Pozostałe pułapki
| Kod | Problem |
|---|---|
static void main() | Małe „m”. C# rozróżnia wielkość liter — kompilator nie znajdzie punktu wejścia i zgłosi CS5001. |
void Main() | Brak static. System musiałby najpierw utworzyć obiekt klasy, żeby wywołać metodę — a nie robi tego. |
Console.Writeline(...) | Małe „l” w Line. Poprawnie: WriteLine. Ten błąd łatwo przeoczyć, bo wygląda prawie dobrze. |
Console.WriteLine("Cześć") | Brak średnika na końcu — CS1002. Każda instrukcja w C# kończy się średnikiem. |
StringBuilder sb = new(); bez using | System.Text nie wchodzi w skład niejawnych using. Trzeba dopisać using System.Text; ręcznie. |
Zadania
Napisz program, który wypisze cztery linijki: Twoje imię, klasę, ulubiony przedmiot i rok. Użyj instrukcji najwyższego poziomu. Ostatnią linijkę wypisz dwiema instrukcjami Console.Write zamiast jedną WriteLine.
pokaż rozwiązanie
Console.WriteLine("Imię: Grzegorz");
Console.WriteLine("Klasa: 2 TI");
Console.WriteLine("Ulubiony przedmiot: programowanie");
Console.Write("Rok: ");
Console.Write(2026);
Console.WriteLine();
Dwa Write skleją się w jedną linię: Rok: 2026. Puste Console.WriteLine() na końcu przenosi kursor niżej — bez niego zachęta systemowa pojawiłaby się tuż za liczbą.
Weź swoje rozwiązanie z zadania 1 i przepisz je na zapis z namespace, class i static void Main. Sprawdź, czy działa tak samo.
pokaż rozwiązanie
using System;
namespace Wizytowka
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Imię: Grzegorz");
Console.WriteLine("Klasa: 2 TI");
Console.WriteLine("Ulubiony przedmiot: programowanie");
Console.Write("Rok: ");
Console.Write(2026);
Console.WriteLine();
}
}
}
Zwróć uwagę na trzy poziomy wcięć — każdy otwarty nawias klamrowy przesuwa kod o jeden poziom w prawo. Visual Studio zrobi to za ciebie skrótem Ctrl+K, Ctrl+D (formatowanie dokumentu).
using System; jest tu opcjonalne, jeśli projekt ma włączone ImplicitUsings — ale w wersji klasycznej pisze się je dla jasności.
Poniższy program nie chce się skompilować. Znajdź wszystkie błędy i je popraw. Wskazówka: są trzy, a jeden dotyczy nie samego kodu, lecz jego układu w pliku.
class Uczen
{
public string Imie
}
Uczen u = new Uczen()
u.Imie = "Ala";
Console.Writeline(u.Imie);
pokaż rozwiązanie
Uczen u = new Uczen(); // 3. dodany średnik + klasa przeniesiona niżej
u.Imie = "Ala";
Console.WriteLine(u.Imie); // 2. WriteLine, nie Writeline
class Uczen
{
public string Imie; // 1. dodany średnik po deklaracji pola
}
Błąd 1 — brak średnika po public string Imie. Deklaracja pola to instrukcja, więc kończy się średnikiem.
Błąd 2 — Console.Writeline zamiast Console.WriteLine. C# rozróżnia wielkość liter.
Błąd 3 — brak średnika po new Uczen() oraz zła kolejność: klasa musi znaleźć się za instrukcjami, inaczej CS8803.
Napisz program, który pyta o szerokość i wysokość prostokąta (liczby całkowite), a następnie wypisuje jego pole i obwód. Zadbaj o to, żeby pytania i odpowiedzi użytkownika były w tych samych liniach.
pokaż rozwiązanie
Console.Write("Podaj szerokość: ");
int szerokosc = int.Parse(Console.ReadLine());
Console.Write("Podaj wysokość: ");
int wysokosc = int.Parse(Console.ReadLine());
int pole = szerokosc * wysokosc;
int obwod = 2 * (szerokosc + wysokosc);
Console.WriteLine($"Pole: {pole}");
Console.WriteLine($"Obwód: {obwod}");
Dla 5 i 3 program wypisze Pole: 15 i Obwód: 16.
Nawiasy w 2 * (szerokosc + wysokosc) są konieczne — bez nich mnożenie wykonałoby się przed dodawaniem i wynik byłby błędny.
Ten program przewróci się, jeśli użytkownik wpisze literę zamiast liczby. Jak sobie z tym poradzić, pokażemy w lekcji 10.
Utwórz nowy projekt konsolowy, zostaw domyślne Console.WriteLine("Hello, World!"); i skompiluj go poleceniem dotnet build. Następnie znajdź w folderze bin plik .dll i otwórz go darmowym narzędziem ILSpy. Znajdź wygenerowaną klasę z metodą Main i porównaj ją z tym, co jest w Program.cs.
pokaż wskazówki
Gdzie szukać pliku: bin\Debug\net8.0\NazwaProjektu.dll (numer wersji zależy od tego, jakie .NET wybrałeś).
Czego szukać w ILSpy: klasy o nazwie <Program>$ — te ostre nawiasy i dolar w nazwie oznaczają, że klasę wygenerował kompilator, a nie człowiek. W środku znajdziesz metodę <Main>$(string[] args) z twoim Console.WriteLine.
Co z tego wynika: instrukcje najwyższego poziomu to naprawdę tylko skrót zapisu. Program po skompilowaniu wygląda identycznie jak wersja klasyczna — również dla systemu operacyjnego, który go uruchamia.
Przy okazji zauważ: obok .dll leży plik .exe. To nie jest twój program — to mały launcher, który uruchamia środowisko .NET i podaje mu .dll do wykonania. Na Linuksie i macOS zamiast .exe jest plik bez rozszerzenia.
Co trzeba zapamiętać
- Każdy program w C# ma punkt wejścia — metodę
Main, od której zaczyna się wykonywanie. - Od C# 9 można pisać instrukcje najwyższego poziomu: kompilator sam opakowuje je w klasę z
Main. - Oba zapisy dają identyczny program — różnią się wyłącznie tym, ile widać rusztowania.
- W pliku z instrukcjami najwyższego poziomu instrukcje muszą poprzedzać deklaracje klas (CS8803).
- Punkt wejścia może być tylko jeden — nie da się połączyć
Mainz instrukcjami najwyższego poziomu. ImplicitUsingsdopisuje najpopularniejsze przestrzenie nazw, ale nie wszystkie —System.TextiSystem.Globalizationtrzeba dodać samemu.- Na egzaminie zawodowym pisz w zapisie klasycznym: jest jednoznaczny i działa wszędzie.