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.
Czego się dziś nauczysz
- Zdefiniujesz klasę z polami i metodami.
- Utworzysz obiekty słowem
newi 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.
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:
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.
class Gracz
{
public string Imie;
public int Punkty;
}
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.
Klasa to przepis, obiekt to wypiek
To rozróżnienie wraca na każdym sprawdzianie, więc warto je zapamiętać obrazkiem.
| Klasa | Obiekt | |
|---|---|---|
| Czym jest | opis: jakie dane i jakie czynności | konkretny egzemplarz z własnymi wartościami |
| Ile ich jest | jedna w całym programie | tyle, ile utworzysz |
| Analogia | przepis na ciasto | upieczone ciasto |
| Zajmuje pamięć | nie | tak — każdy obiekt osobno |
| Zapis w kodzie | class Gracz { … } | new Gracz() |
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.
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.
class Gracz
{
public string Imie;
public int Punkty;
public void Dodaj(int ile)
{
Punkty += ile;
}
public string Opis()
{
return $"{Imie}: {Punkty} pkt";
}
}
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.
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.
Gdzie w pliku zapisać klasę
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.
| Zasada | Dlaczego |
|---|---|
| Jedna klasa — jeden plik | tak 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, rzeczownik | Gracz, Magazyn, Bilet — klasa opisuje rzecz, nie czynność |
Klasa poza Main | definicja 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: dwóch graczy w jednym turnieju
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()}");
}
}
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
Dodajsama pilnuje sensu danych — ujemna liczba punktów zostaje odrzucona. Wcześniej takie sprawdzenie musiałby powtarzać każdy, kto zmienia punkty. Wygrywaprzyjmuje innego gracza jako parametr. Klasa może być typem parametru tak samo jakintczystring.- Wewnątrz
WygrywazapisPunktyoznacza punkty obiektu, na którym metodę wywołano, ainny.Punkty— punkty przekazanego gracza. - Ostatnie trzy wiersze to najważniejsza pułapka tej lekcji:
kopia = alanie 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.
Co robi każdy użyty element
| Element | Znaczenie | Uwagi |
|---|---|---|
class Nazwa { … } | definicja klasy | Opis typu, nie obiekt. Zapisujemy poza Main, nazwa w PascalCase. |
new Nazwa() | utworzenie obiektu | Rezerwuje pamięć i zwraca odwołanie. Każde wywołanie daje nowy egzemplarz. |
public string Imie; | pole | Dana należąca do obiektu. Szczegóły w lekcji 38. |
public void Metoda() | metoda obiektu | Brak static — wywołujemy przez kropkę na obiekcie. |
obiekt.Skladowa | dostęp przez kropkę | Kropka czyta się „tego obiektu”. Na null daje NullReferenceException. |
this | bieżący obiekt | Opcjonalne, dopóki nazwy się nie powtarzają. |
new Gracz { Imie = "Ala" } | inicjalizator obiektu | Tworzy obiekt i od razu ustawia pola. |
Gracz jako typ parametru | — | Własna klasa może być typem parametru, zmiennej i wyniku metody. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Gracz a; bez new | Zmienna 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ól | CS0120 — metoda statyczna nie wie, na którym obiekcie miałaby działać. |
Klasa zdefiniowana wewnątrz Main | Powstaje typ lokalny. Sprawdź, gdzie stoi klamra zamykająca metodę. |
class gracz | Kompiluje się, ale łamie konwencję z lekcji 05. Nazwy typów piszemy PascalCase. |
b = a; zamiast nowego obiektu | Obie nazwy wskazują ten sam obiekt — zmiana przez jedną widoczna jest przez drugą (lekcja 43). |
| Klasa z jednym polem i bez metod | Zwykle znak, że obiekt nie był potrzebny. Klasa ma sens, gdy łączy dane z czynnościami na nich. |
Zadania
Napisz klasę Ksiazka z polami Tytul, Autor i Rok. W Main utwórz dwa obiekty, ustaw ich pola i wypisz dane obu.
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.
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.
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.
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.
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.
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
staticnależy do obiektu i wywołujemy ją przez kropkę. - Wewnątrz metody obiektu odwołujemy się do pól po samej nazwie;
thisjest opcjonalne. - Sama deklaracja zmiennej nie tworzy obiektu — bez
newjest tamnull. - Przypisanie obiektu do drugiej zmiennej nie kopiuje go; obie nazwy wskazują to samo.
Dokumentacja: Microsoft Learn — temat tej lekcji.