Referencje i kopie — jeden obiekt pod dwiema nazwami
Dwie osoby mogą mieć kartkę z tym samym numerem szafki. Skopiowanie kartki nie buduje drugiej szafki — i dokładnie to dzieje się przy przypisaniu obiektu do drugiej zmiennej.
Czego się dziś nauczysz
- Odróżnisz typ wartościowy od referencyjnego i podasz przykłady obu.
- Policzysz, ile obiektów naprawdę istnieje w programie.
- Wskażesz, co metoda może zmienić w przekazanym obiekcie, a czego nie.
- Wyjaśnisz wynik porównania dwóch obiektów operatorem
==. - Odróżnisz kopię płytką od głębokiej i napiszesz obie.
Przygotowanie: lekcje 01–42. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Zmienna trzyma wartość albo wskazanie
To najważniejsze rozróżnienie w całym bloku obiektowym. Wszystko, co dalej, z niego wynika.
| Typ wartościowy | Typ referencyjny | |
|---|---|---|
| Co trzyma zmienna | samą wartość | wskazanie na obiekt |
| Przykłady | int, double, bool, char, struct | class, string, tablica, List<T> |
| Przypisanie do drugiej zmiennej | kopiuje wartość | kopiuje wskazanie — obiekt zostaje jeden |
| Przekazanie do metody | metoda dostaje kopię | metoda sięga do tego samego obiektu |
| Wartość domyślna | 0, false | null |
// typ wartosciowy
int a = 5;
int b = a; // skopiowana wartosc
b = 99;
Console.WriteLine(a); // 5 - a nietkniete
// typ referencyjny
Robot x = new Robot();
Robot y = x; // skopiowane WSKAZANIE
y.Energia = 99;
Console.WriteLine(x.Energia); // 99 - to ten sam robot
Numer szafki
Zmienna typu referencyjnego to kartka z numerem szafki, a nie sama szafka. Skopiowanie kartki daje drugą kartkę z tym samym numerem — a nie drugą szafkę. Otwierając szafkę z którejkolwiek kartki, widzisz tę samą zawartość.
Licz new, nie zmienne
Pudelko a = new Pudelko(); // obiekt 1
Pudelko b = a; // nadal obiekt 1
Pudelko c = new Pudelko(); // obiekt 2
Pudelko d = c; // nadal obiekt 2
Cztery zmienne, dwa obiekty. Reguła jest prosta: obiektów jest tyle, ile razy wykonano new. Przypisanie nie tworzy niczego.
a.Liczba = 10;
Console.WriteLine(b.Liczba); // 10 - a i b to jedno pudelko
Console.WriteLine(c.Liczba); // 0 - c to inne pudelko
b = c; // b przestaje wskazywac pierwsze pudelko
Console.WriteLine(b.Liczba); // 0
Porównywanie
Pudelko p1 = new Pudelko { Liczba = 5 };
Pudelko p2 = new Pudelko { Liczba = 5 };
Pudelko p3 = p1;
Console.WriteLine(p1 == p2); // False - dwa rozne obiekty
Console.WriteLine(p1 == p3); // True - to samo wskazanie
string s1 = "Ala";
string s2 = "Ala";
Console.WriteLine(s1 == s2); // True - string jest wyjatkiem
string zachowuje się inaczej
Napis jest typem referencyjnym, ale operator == porównuje dla niego zawartość, a nie wskazanie — to celowy wyjątek wbudowany w język. Dla własnych klas porównywana jest tożsamość, dopóki nie nadpiszesz Equals (lekcja 51).
Co metoda może zmienić
static void Zmien(int liczba, Robot robot, Robot nowy)
{
liczba = 99; // kopia - bez wplywu na zewnatrz
robot.Energia = 99; // TEN SAM obiekt - zmiana widoczna
nowy = new Robot(); // podmiana WSKAZANIA w kopii - bez wplywu
}
int x = 1;
Robot r = new Robot();
Robot n = new Robot();
Zmien(x, r, n);
Console.WriteLine(x); // 1 - bez zmian
Console.WriteLine(r.Energia); // 99 - zmienione
Console.WriteLine(n == null); // False - n wskazuje stary obiekt
Trzy parametry, trzy różne wyniki. Warto je rozdzielić:
| Co robi metoda | Widoczne na zewnątrz? | Dlaczego |
|---|---|---|
| zmienia parametr typu wartościowego | nie | metoda dostała kopię wartości |
| zmienia pole przekazanego obiektu | tak | wskazanie prowadzi do tego samego obiektu |
podmienia cały obiekt przez new | nie | metoda ma własną kopię wskazania |
podmienia obiekt przy parametrze ref | tak | ref daje dostęp do samej zmiennej |
Najczęstsze nieporozumienie
„Przekazałem obiekt przez referencję” brzmi jak ref, ale to co innego. Domyślnie do metody trafia kopia wskazania — wystarczy, żeby zmienić zawartość obiektu, ale za mało, żeby podmienić go na inny. Do podmiany potrzebne jest ref z lekcji 20.
Kiedy kopia nie jest kopią
Konstruktor kopiujący z lekcji 40 przepisuje pola. Przy polach liczbowych i tekstowych to wystarcza. Przy polu będącym obiektem — już nie.
class Zespol
{
public string Nazwa;
public List<string> Czlonkowie = new List<string>();
public Zespol(Zespol wzor)
{
Nazwa = wzor.Nazwa; // kopia napisu - w porzadku
Czlonkowie = wzor.Czlonkowie; // KOPIA WSKAZANIA - jedna lista!
}
}
Zespol a = new Zespol { Nazwa = "Alfa" };
a.Czlonkowie.Add("Ala");
Zespol b = new Zespol(a);
b.Nazwa = "Beta";
b.Czlonkowie.Add("Olek");
Console.WriteLine(a.Nazwa); // Alfa - nazwy sa niezalezne
Console.WriteLine(a.Czlonkowie.Count); // 2 - lista jest wspolna!
Żeby kopia była naprawdę niezależna, trzeba utworzyć nową listę:
public Zespol(Zespol wzor)
{
Nazwa = wzor.Nazwa;
Czlonkowie = new List<string>(wzor.Czlonkowie); // kopia glaboka
}
| Rodzaj kopii | Co powstaje | Kiedy wystarcza |
|---|---|---|
| płytka | nowy obiekt, ale pola-obiekty wspólne | gdy pola to liczby i napisy albo gdy współdzielenie jest zamierzone |
| głęboka | nowy obiekt i nowe kopie pól-obiektów | gdy kopia ma żyć własnym życiem |
Zapisz decyzję w komentarzu
Nie ma reguły mówiącej, która kopia jest lepsza — to zależy od modelu. Jest natomiast reguła praktyczna: przy każdym konstruktorze kopiującym dopisz jedno zdanie o tym, co jest współdzielone, a co nie. Oszczędza godzinę szukania błędu pół roku później.
Przykład: dwie nazwy, jedna szafka
using System.Collections.Generic;
class Zespol
{
public string Nazwa = "";
public List<string> Czlonkowie = new List<string>();
public Zespol Kopiuj(bool gleboko)
{
Zespol nowy = new Zespol();
nowy.Nazwa = Nazwa;
nowy.Czlonkowie = gleboko
? new List<string>(Czlonkowie)
: Czlonkowie;
return nowy;
}
public string Opis() => $"{Nazwa}: {string.Join(", ", Czlonkowie)}";
}
class Program
{
static void DodajCzlonka(Zespol zespol, string imie)
{
zespol.Czlonkowie.Add(imie); // zmienia TEN obiekt
}
static void PodmienZespol(Zespol zespol)
{
zespol = new Zespol { Nazwa = "Podmieniony" }; // bez wplywu na zewnatrz
}
static void Main()
{
Zespol alfa = new Zespol { Nazwa = "Alfa" };
alfa.Czlonkowie.Add("Ala");
Zespol tenSam = alfa; // to samo wskazanie
tenSam.Nazwa = "Alfa (nowa nazwa)";
Console.WriteLine(alfa.Opis());
DodajCzlonka(alfa, "Olek");
Console.WriteLine(alfa.Opis());
PodmienZespol(alfa);
Console.WriteLine(alfa.Opis());
Console.WriteLine();
Zespol plytka = alfa.Kopiuj(false);
plytka.Czlonkowie.Add("Ewa");
Console.WriteLine("po plytkiej: " + alfa.Opis());
Zespol gleboka = alfa.Kopiuj(true);
gleboka.Czlonkowie.Add("Jan");
Console.WriteLine("po glebokiej: " + alfa.Opis());
Console.WriteLine("kopia: " + gleboka.Opis());
}
}
Alfa (nowa nazwa): Ala
Alfa (nowa nazwa): Ala, Olek
Alfa (nowa nazwa): Ala, Olek
po plytkiej: Alfa (nowa nazwa): Ala, Olek, Ewa
po glebokiej: Alfa (nowa nazwa): Ala, Olek, Ewa
kopia: Alfa (nowa nazwa): Ala, Olek, Ewa, Jan
Co dzieje się po kolei
tenSamto druga nazwa tego samego zespołu — zmiana nazwy widoczna jest przez obie.DodajCzlonkazmienia zawartość przekazanego obiektu, więc efekt widać po powrocie z metody.PodmienZespoltworzy nowy obiekt, ale przypisuje go do własnej kopii wskazania. Na zewnątrz nie zmienia się nic.- Po kopii płytkiej dodanie Ewy do kopii pojawiło się w oryginale — obie korzystają z jednej listy.
- Po kopii głębokiej dodanie Jana zostało tylko w kopii. Dopiero to jest niezależny obiekt.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
| typ wartościowy | zmienna trzyma wartość | int, double, bool, struct — kopiowane przy przypisaniu. |
| typ referencyjny | zmienna trzyma wskazanie | class, tablica, List<T>, string. |
b = a; | kopia wskazania | Obiekt zostaje jeden; liczba obiektów to liczba wywołań new. |
== dla klas | porównanie tożsamości | Sprawdza, czy to ten sam obiekt, nie czy ma taką samą zawartość. |
== dla string | porównanie zawartości | Wyjątek wbudowany w język. |
| parametr zwykły | kopia wskazania | Wolno zmienić zawartość obiektu, nie wolno podmienić obiektu. |
parametr ref | dostęp do zmiennej | Pozwala podmienić cały obiekt (lekcja 20). |
new List<T>(inna) | kopia kolekcji | Podstawowe narzędzie kopii głębokiej. |
null | brak wskazania | Użycie kropki kończy się NullReferenceException (lekcja 34). |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Robot b = a; w celu skopiowania | Powstaje druga nazwa, nie drugi obiekt. Kopia wymaga new. |
| Liczenie obiektów po zmiennych | Cztery zmienne mogą wskazywać dwa obiekty. Licz wywołania new. |
p1 == p2 dla dwóch identycznych obiektów | False — porównywana jest tożsamość. Do porównania zawartości nadpisz Equals (lekcja 51). |
| Oczekiwanie, że metoda podmieni obiekt | Bez ref metoda pracuje na kopii wskazania. |
| Konstruktor kopiujący przepisujący listę | To kopia płytka — obie „kopie” korzystają z jednej listy. |
readonly na liście traktowane jak ochrona zawartości | Chroni tylko przed podmianą listy na inną. |
Porównywanie tablic operatorem == | Porównuje wskazania. Do porównania zawartości służy SequenceEqual (lekcja 24). |
Zadania
Napisz program, w którym pięć zmiennych typu Pudelko wskazuje na trzy obiekty. Zmień wartość w jednym z nich i wypisz wszystkie pięć, żeby pokazać, które zmienne są powiązane. Narysuj to na kartce strzałkami, zanim uruchomisz.
Napisz metodę zmieniającą parametr typu int i drugą zmieniającą pole przekazanego obiektu. Wypisz wartości przed i po wywołaniu obu i wyjaśnij różnicę w komentarzu.
Napisz metodę, która próbuje podmienić przekazany obiekt na nowy, i pokaż, że na zewnątrz nic się nie zmienia. Następnie dopisz ref i porównaj wynik.
Utwórz dwa obiekty o identycznej zawartości i trzecią zmienną wskazującą na pierwszy z nich. Wypisz wyniki wszystkich porównań operatorem ==. Dodaj do zestawu dwa napisy o tej samej treści i wyjaśnij, dlaczego wynik jest inny.
Napisz klasę Zespol z listą członków i dwie metody kopiujące: płytką i głęboką. Pokaż na przykładzie, przy której z nich zmiana w kopii wpływa na oryginał.
Napisz klasę Szkola zawierającą listę obiektów klasy Klasa, z których każdy ma własną listę uczniów. Zrób kopię głęboką całej szkoły — taką, w której dodanie ucznia do kopii nie zmienia oryginału na żadnym poziomie. Sprawdź to programem i zapisz, ile poziomów kopiowania trzeba było obsłużyć.
Co trzeba zapamiętać
- Typ wartościowy trzyma wartość, typ referencyjny — wskazanie na obiekt.
- Przypisanie obiektu kopiuje wskazanie; obiektów jest tyle, ile wywołań
new. - Metoda może zmienić zawartość przekazanego obiektu, ale nie podmieni go bez
ref. ==dla własnych klas porównuje tożsamość;stringjest wyjątkiem.- Kopia płytka dzieli pola-obiekty z oryginałem, głęboka tworzy ich własne kopie.
readonlychroni wskazanie, nigdy zawartość kolekcji.
Dokumentacja: Microsoft Learn — temat tej lekcji.