Pierwszy program: Main czy instrukcje najwyższego poziomu?

Otwierasz nowy projekt konsolowy w Visual Studio i widzisz krótki program 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. Metody można deklarować między innymi w klasach i strukturach; tutaj używamy klasy — 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 ma jeden wybrany punkt wejścia. Kilka pasujących metod Main wymaga wskazania punktu startu w ustawieniach projektu. Jeśli są instrukcje najwyższego poziomu, to one stanowią punkt wejścia, a jawna metoda Main jest ignorowana jako punkt startu (ostrzeżenie CS7022).

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”)
Zadanie szkolne lub egzaminacyjneZgodny z poleceniem i środowiskiem; samo Main nie ogranicza wersji składni użytej w środku
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 zwraca tekst albo null — 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

ostrzeżenie CS7022 — kod najwyższego poziomu ma pierwszeństwo
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. Łączenie obu jest możliwe, ale wprowadza niepotrzebne zamieszanie: jawne Main nie zostanie wybrane jako punkt startu.

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. Wywołanie metody jako instrukcja wymaga średnika; bloki, np. if z klamrami, nie mają takiego zakończenia.
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.

ZAD 2Przepisz na wersję klasyczną★☆☆

Poniższy program zapisano w postaci instrukcji najwyższego poziomu. Przepisz go na zapis klasyczny — z namespace, class i static void Main — tak, żeby działał dokładnie tak samo.

do przepisania
Console.WriteLine("Dziennik klasy 4TP");
Console.WriteLine("---------------------");
Console.WriteLine("Uczniow: 24");

Wynik przed przepisaniem i po przepisaniu musi być identyczny — porównaj oba uruchomienia.

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. Klasami zajmiemy się dopiero w lekcji 37 tutaj poprawiasz wyłącznie składnię i kolejność elementów w pliku, nie musisz rozumieć, czym jest klasa.

class Uczen
{
    public string Imie
}

Uczen u = new Uczen()
u.Imie = "Ala";
Console.Writeline(u.Imie);
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.

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Aplikacja wykonywalna potrzebuje wybranego punktu wejścia. Biblioteka klas nie potrzebuje Main.
  • Instrukcje najwyższego poziomu pozwalają pominąć jawną klasę Program i metodę startową.
  • Instrukcje najwyższego poziomu mogą być w jednym pliku projektu i muszą poprzedzać deklaracje typów w tym pliku.
  • Gdy są instrukcje najwyższego poziomu, jawna metoda Main nie jest wybierana jako punkt wejścia: pojawia się ostrzeżenie CS7022.
  • Kilka pasujących metod Main wymaga jednoznacznego wskazania punktu startu.
  • ImplicitUsings udostępnia wybrane przestrzenie nazw. System.Text i System.Globalization zwykle dodajemy jawnie.
  • W zadaniu szkolnym lub egzaminacyjnym dobierz zapis do polecenia i dostępnego środowiska.