Klasy i obiekty — dane, które wiedzą, do kogo należą

Dwóch graczy to cztery zmienne z numerkami, pięciu — dziesięć. Klasa pozwala powiedzieć, że imię i punkty to jedna rzecz, a nie dwie osobne dane, które trzeba kojarzyć w głowie.

C# class new obiekt 45–90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zdefiniujesz klasę z polami i metodami.
  • Utworzysz obiekty słowem new i wyjaśnisz, czym różnią się od klasy.
  • Wywołasz metodę na konkretnym obiekcie.
  • Wyjaśnisz, dlaczego metoda obiektu nie ma static.
  • Rozpoznasz sytuację, w której dwie zmienne wskazują ten sam obiekt.

Przygotowanie: lekcje 01–36. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Dane, które nie wiedzą, do kogo należą

Turniej ma dwóch graczy. Każdy ma imię i punkty. Bez klas wygląda to tak:

dwóch graczy, cztery zmienne
string imie1 = "Ala";
int punkty1 = 0;

string imie2 = "Olek";
int punkty2 = 0;

punkty1 += 5;

Console.WriteLine($"{imie1}: {punkty1}");
Console.WriteLine($"{imie2}: {punkty2}");

Przy dwóch graczach jeszcze się da. Przy pięciu mamy dziesięć zmiennych z numerkami, a przy każdej operacji trzeba pamiętać, żeby zmieniać te z tym samym numerem. Zapis punkty1 += 5 obok imie2 skompiluje się bez mrugnięcia okiem.

Klasa pozwala powiedzieć: imię i punkty to nie są dwie osobne zmienne, tylko jedna rzecz — gracz.

definicja typu
class Gracz
{
    public string Imie;
    public int Punkty;
}
użycie
Gracz a = new Gracz();
a.Imie = "Ala";
a.Punkty = 0;

Gracz b = new Gracz();
b.Imie = "Olek";
b.Punkty = 0;

a.Punkty += 5;

Console.WriteLine($"{a.Imie}: {a.Punkty}");
Console.WriteLine($"{b.Imie}: {b.Punkty}");

Wynik jest identyczny. Zmieniło się co innego: teraz nie da się przez pomyłkę dopisać punktów Ali do imienia Olka, bo punkty należą do konkretnego gracza.

Obiekty nie zmieniają wyniku, tylko porządek

To ważne, żeby nie szukać magii tam, gdzie jej nie ma. Programowanie obiektowe nie sprawia, że program liczy lepiej. Sprawia, że dane i czynności, które do siebie należą, leżą w jednym miejscu — a przez to kod da się czytać, zmieniać i sprawdzać po kawałku.

TEORIA

Klasa to przepis, obiekt to wypiek

To rozróżnienie wraca na każdym sprawdzianie, więc warto je zapamiętać obrazkiem.

KlasaObiekt
Czym jestopis: jakie dane i jakie czynnościkonkretny egzemplarz z własnymi wartościami
Ile ich jestjedna w całym programietyle, ile utworzysz
Analogiaprzepis na ciastoupieczone ciasto
Zajmuje pamięćnietak — każdy obiekt osobno
Zapis w kodzieclass Gracz { … }new Gracz()
Program.cs
Gracz a = new Gracz();     // pierwszy obiekt
Gracz b = new Gracz();     // drugi, zupelnie niezalezny

a.Punkty = 5;

Console.WriteLine(a.Punkty);   // 5
Console.WriteLine(b.Punkty);   // 0  - b nic o tym nie wie

Słowo new robi dwie rzeczy: rezerwuje w pamięci miejsce na nowy obiekt i zwraca do niego odwołanie, które zapisujemy w zmiennej. Każde new to nowy, osobny egzemplarz.

Sama deklaracja nie tworzy obiektu

Zapis Gracz a; deklaruje zmienną, ale nie tworzy obiektu — zmienna wskazuje na null. Próba użycia a.Punkty kończy się wtedy wyjątkiem NullReferenceException, który poznałeś w lekcji 34. Obiekt powstaje dopiero przy new.

TEORIA

Czynności też należą do obiektu

Na razie klasa trzyma tylko dane. Prawdziwa zmiana przychodzi wtedy, gdy trafiają do niej także czynności wykonywane na tych danych.

definicja typu
class Gracz
{
    public string Imie;
    public int Punkty;

    public void Dodaj(int ile)
    {
        Punkty += ile;
    }

    public string Opis()
    {
        return $"{Imie}: {Punkty} pkt";
    }
}
Program.cs
Gracz a = new Gracz();
a.Imie = "Ala";

a.Dodaj(5);
a.Dodaj(3);

Console.WriteLine(a.Opis());     // Ala: 8 pkt

Zwróć uwagę na dwie rzeczy w metodzie Dodaj. Po pierwsze: nie ma słowa static — to metoda obiektu, więc wywołujemy ją na konkretnym graczu, a nie samodzielnie. Po drugie: w środku piszemy po prostu Punkty, bez żadnego przedrostka. Metoda działa na polach tego obiektu, na którym ją wywołano.

Program.cs
Gracz a = new Gracz { Imie = "Ala" };
Gracz b = new Gracz { Imie = "Olek" };

a.Dodaj(5);      // rosna punkty Ali
b.Dodaj(2);      // rosna punkty Olka

Console.WriteLine(a.Opis());   // Ala: 5 pkt
Console.WriteLine(b.Opis());   // Olek: 2 pkt

Słowo this

Gdy chcesz podkreślić, że chodzi o pole bieżącego obiektu, możesz napisać this.Punkty. Znaczy dokładnie to samo. this staje się konieczne dopiero wtedy, gdy parametr metody nazywa się tak samo jak pole — zobaczysz to w lekcji 40 przy konstruktorach.

static kontra metoda obiektu

Metoda static należy do klasy i nie widzi pól obiektu — próba użycia Punkty w metodzie statycznej daje błąd CS0120. Metoda bez static należy do obiektu i wymaga wywołania przez kropkę: a.Dodaj(5). Do tej pory pisałeś wyłącznie metody static, bo nie było jeszcze żadnych obiektów.

TEORIA

Gdzie w pliku zapisać klasę

Program.cs — cały plik
class Gracz
{
    public string Imie;
    public int Punkty;

    public void Dodaj(int ile)
    {
        Punkty += ile;
    }
}

class Program
{
    static void Main()
    {
        Gracz a = new Gracz();
        a.Imie = "Ala";
        a.Dodaj(5);

        Console.WriteLine($"{a.Imie}: {a.Punkty}");
    }
}

Obie klasy stoją na tym samym poziomie, jedna obok drugiej. Klasa Gracz nie jest w środku klasy Program ani w środku Main.

ZasadaDlaczego
Jedna klasa — jeden pliktak robi się w prawdziwych projektach; nazwa pliku odpowiada nazwie klasy
W kursie klasy mogą być w jednym plikużeby dało się wkleić i uruchomić jeden przykład bez zakładania projektu z wieloma plikami
Nazwa klasy PascalCase, rzeczownikGracz, Magazyn, Bilet — klasa opisuje rzecz, nie czynność
Klasa poza Maindefinicja wewnątrz metody tworzy typ lokalny widoczny tylko tam — nie o to chodzi

Skrócone tworzenie z wartościami

Zapis new Gracz { Imie = "Ala", Punkty = 3 } tworzy obiekt i od razu ustawia pola. To inicjalizator obiektu — wygodny skrót, gdy pól jest kilka. W lekcji 40 zobaczysz, czym różni się od konstruktora i kiedy który wybrać.

PRZYKŁAD

Przykład: dwóch graczy w jednym turnieju

Program.cs
class Gracz
{
    public string Imie;
    public int Punkty;

    public void Dodaj(int ile)
    {
        if (ile <= 0)
        {
            Console.WriteLine($"  {Imie}: odrzucono {ile} pkt");
            return;
        }

        Punkty += ile;
    }

    public bool Wygrywa(Gracz inny)
    {
        return Punkty > inny.Punkty;
    }

    public string Opis()
    {
        return $"{Imie,-6} {Punkty,3} pkt";
    }
}

class Program
{
    static void Main()
    {
        Gracz ala = new Gracz { Imie = "Ala" };
        Gracz olek = new Gracz { Imie = "Olek" };

        ala.Dodaj(5);
        ala.Dodaj(3);
        olek.Dodaj(6);
        olek.Dodaj(-2);          // odrzucone

        Console.WriteLine(ala.Opis());
        Console.WriteLine(olek.Opis());

        Console.WriteLine();
        Console.WriteLine($"Ala wygrywa z Olkiem: {ala.Wygrywa(olek)}");
        Console.WriteLine($"Olek wygrywa z Ala:   {olek.Wygrywa(ala)}");

        Gracz kopia = ala;       // UWAGA: to nie jest nowy gracz
        kopia.Dodaj(100);
        Console.WriteLine($"\nPo dodaniu do kopii: {ala.Opis()}");
    }
}
wynik w konsoli
Olek: odrzucono -2 pkt
Ala      8 pkt
Olek     6 pkt

Ala wygrywa z Olkiem: True
Olek wygrywa z Ala:   False

Po dodaniu do kopii: Ala    108 pkt

Co dzieje się po kolei

  • Metoda Dodaj sama pilnuje sensu danych — ujemna liczba punktów zostaje odrzucona. Wcześniej takie sprawdzenie musiałby powtarzać każdy, kto zmienia punkty.
  • Wygrywa przyjmuje innego gracza jako parametr. Klasa może być typem parametru tak samo jak int czy string.
  • Wewnątrz Wygrywa zapis Punkty oznacza punkty obiektu, na którym metodę wywołano, a inny.Punkty — punkty przekazanego gracza.
  • Ostatnie trzy wiersze to najważniejsza pułapka tej lekcji: kopia = ala nie tworzy nowego gracza. Obie nazwy wskazują na ten sam obiekt, więc Ala ma nagle 108 punktów. Dlaczego tak jest, wyjaśnia lekcja 43.
ELEMENTY WBUDOWANE

Co robi każdy użyty element

ElementZnaczenieUwagi
class Nazwa { … }definicja klasyOpis typu, nie obiekt. Zapisujemy poza Main, nazwa w PascalCase.
new Nazwa()utworzenie obiektuRezerwuje pamięć i zwraca odwołanie. Każde wywołanie daje nowy egzemplarz.
public string Imie;poleDana należąca do obiektu. Szczegóły w lekcji 38.
public void Metoda()metoda obiektuBrak static — wywołujemy przez kropkę na obiekcie.
obiekt.Skladowadostęp przez kropkęKropka czyta się „tego obiektu”. Na null daje NullReferenceException.
thisbieżący obiektOpcjonalne, dopóki nazwy się nie powtarzają.
new Gracz { Imie = "Ala" }inicjalizator obiektuTworzy obiekt i od razu ustawia pola.
Gracz jako typ parametruWłasna klasa może być typem parametru, zmiennej i wyniku metody.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Gracz a; bez newZmienna wskazuje null; pierwsze użycie kropki daje NullReferenceException.
Gracz.Punkty = 5;Punkty należą do obiektu, nie do klasy. Trzeba wskazać konkretny obiekt: a.Punkty.
static przy metodzie używającej pólCS0120 — metoda statyczna nie wie, na którym obiekcie miałaby działać.
Klasa zdefiniowana wewnątrz MainPowstaje typ lokalny. Sprawdź, gdzie stoi klamra zamykająca metodę.
class graczKompiluje się, ale łamie konwencję z lekcji 05. Nazwy typów piszemy PascalCase.
b = a; zamiast nowego obiektuObie nazwy wskazują ten sam obiekt — zmiana przez jedną widoczna jest przez drugą (lekcja 43).
Klasa z jednym polem i bez metodZwykle znak, że obiekt nie był potrzebny. Klasa ma sens, gdy łączy dane z czynnościami na nich.
ZADANIA

Zadania

ZAD 1Klasa Ksiazka★☆☆

Napisz klasę Ksiazka z polami Tytul, Autor i Rok. W Main utwórz dwa obiekty, ustaw ich pola i wypisz dane obu.

ZAD 2Metoda opisu★☆☆

Dodaj do klasy Ksiazka metodę Opis() zwracającą tekst w postaci Lem, Solaris (1961). Wypisz opisy obu książek, wywołując metodę zamiast składać tekst w Main.

ZAD 3Licznik kliknięć★☆☆

Napisz klasę Licznik z polem Wartosc oraz metodami Zwieksz() i Zeruj(). Utwórz dwa niezależne liczniki, zwiększ pierwszy trzy razy, drugi raz, i wypisz oba wyniki, żeby pokazać, że są niezależne.

ZAD 4Konto z ochroną★★☆

Napisz klasę Konto z polem Saldo i metodami Wplata(decimal) oraz Wyplata(decimal). Wypłata większa od salda ma zostać odrzucona komunikatem, bez zmiany salda; kwota ujemna również. Sprawdź program na czterech przypadkach, w tym obu błędnych.

ZAD 5Porównanie obiektów★★☆

Do klasy Ksiazka dodaj metodę bool StarszaNiz(Ksiazka inna) porównującą rok wydania. Utwórz trzy książki i wypisz wyniki porównań każdej z każdą. Zastanów się, co metoda powinna zwrócić dla dwóch książek z tego samego roku, i zapisz decyzję w komentarzu.

ZAD 6Termometr z historią★★★

Napisz klasę Termometr z polami przechowującymi ostatni pomiar, najwyższy i najniższy odczyt oraz liczbę pomiarów. Metoda Zmierz(double) ma aktualizować wszystkie te wartości, a metoda Raport() zwracać podsumowanie z wartością średnią. Zadbaj o to, żeby raport przed pierwszym pomiarem nie pokazywał bezsensownych liczb ani nie dzielił przez zero.

PODSUMOWANIE

Co trzeba zapamiętać

  • Klasa to opis typu, obiekt to konkretny egzemplarz utworzony przez new.
  • Każdy obiekt ma własny komplet pól — zmiana w jednym nie dotyczy drugiego.
  • Metoda bez static należy do obiektu i wywołujemy ją przez kropkę.
  • Wewnątrz metody obiektu odwołujemy się do pól po samej nazwie; this jest opcjonalne.
  • Sama deklaracja zmiennej nie tworzy obiektu — bez new jest tam null.
  • Przypisanie obiektu do drugiej zmiennej nie kopiuje go; obie nazwy wskazują to samo.

Dokumentacja: Microsoft Learn — temat tej lekcji.