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.

C# Main top-level statements 30 min
CEL LEKCJI

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
TEORIA

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:

Program.cs — to generuje Visual Studio
// 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:

Program.cs — zapis klasyczny
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:

MojProjekt.csproj
<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.

TEORIA

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.

ElementCo oznacza
using System;Udostępnia nazwy z przestrzeni System, dzięki czemu można pisać Console zamiast System.Console.
namespace HelloWorldPrzestrzeń nazw — logiczny „folder” na klasy. Chroni przed konfliktem, gdy dwie biblioteki mają klasę o tej samej nazwie.
class ProgramKlasa. W C# każdy kod musi znajdować się w jakiejś klasie — nazwa Program to tylko konwencja, można ją zmienić.
staticMetoda należy do klasy, nie do obiektu. Dzięki temu system może wywołać Main bez tworzenia obiektu klasy Program.
voidMetoda nic nie zwraca. Można też napisać int — wtedy zwracana liczba staje się kodem wyjścia programu (0 oznacza sukces).
MainPunkt wejścia — miejsce, od którego zaczyna się wykonywanie programu. Pisane wielką literą.
string[] argsArgumenty 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ć?

SytuacjaZapis
Krótki program ćwiczeniowy, jeden plikInstrukcje najwyższego poziomu — mniej pisania, mniej rozpraszaczy
Program z własnymi klasamiOba działają, ale pilnuj kolejności (patrz „Częste błędy”)
Egzamin zawodowy INF.03 / INF.04Klasyczny 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ć.

UŻYTE ELEMENTY

Co dokładnie robi każdy użyty element

ElementZwracaDział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.
argsstring[] 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.
PRZYKŁAD Z OMÓWIENIEM

Ten sam program napisany na dwa sposoby

Program pyta o imię i rok urodzenia, po czym wypisuje powitanie i wiek. Najpierw krótko:

Program.cs — instrukcje najwyższego poziomu
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:

Program.cs — zapis klasyczny
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.");
        }
    }
}
wynik w konsoli (oba programy)
Jak masz na imię? Grzegorz
W którym roku się urodziłeś? 2008
Cześć, Grzegorz! W tym roku kończysz 18 lat.

Omówienie

FragmentDlaczego 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.

CZĘSTE BŁĘDY

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:

błąd — program się nie skompiluje
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:

poprawnie
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

błąd — kompilator nie wie, od czego zacząć
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

KodProblem
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 usingSystem.Text nie wchodzi w skład niejawnych using. Trzeba dopisać using System.Text; ręcznie.
ZADANIA

Zadania

ZAD 1Wizytówka★☆☆

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ą.

ZAD 2Przepisz na wersję klasyczną★☆☆

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.

ZAD 3Znajdź trzy błędy★★☆

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 2Console.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.

ZAD 4Kalkulator pola prostokąta★★☆

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.

ZAD 5Zajrzyj pod maskę★★★

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.

PODSUMOWANIE

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ć Main z instrukcjami najwyższego poziomu.
  • ImplicitUsings dopisuje najpopularniejsze przestrzenie nazw, ale nie wszystkieSystem.Text i System.Globalization trzeba dodać samemu.
  • Na egzaminie zawodowym pisz w zapisie klasycznym: jest jednoznaczny i działa wszędzie.