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.

C# referencje kopia płytka kopia głęboka 45–90 min
CEL LEKCJI

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.

TEORIA

Zmienna trzyma wartość albo wskazanie

To najważniejsze rozróżnienie w całym bloku obiektowym. Wszystko, co dalej, z niego wynika.

Typ wartościowyTyp referencyjny
Co trzyma zmiennasamą wartośćwskazanie na obiekt
Przykładyint, double, bool, char, structclass, string, tablica, List<T>
Przypisanie do drugiej zmiennejkopiuje wartośćkopiuje wskazanie — obiekt zostaje jeden
Przekazanie do metodymetoda dostaje kopięmetoda sięga do tego samego obiektu
Wartość domyślna0, falsenull
Program.cs
// 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ść.

TEORIA

Licz new, nie zmienne

Program.cs
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.

Program.cs
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

Program.cs
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).

TEORIA

Co metoda może zmienić

definicja metody i użycie
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 metodaWidoczne na zewnątrz?Dlaczego
zmienia parametr typu wartościowegoniemetoda dostała kopię wartości
zmienia pole przekazanego obiektutakwskazanie prowadzi do tego samego obiektu
podmienia cały obiekt przez newniemetoda ma własną kopię wskazania
podmienia obiekt przy parametrze reftakref 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.

TEORIA

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.

kopia płytka
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!
    }
}
Program.cs
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ę:

kopia głęboka
public Zespol(Zespol wzor)
{
    Nazwa = wzor.Nazwa;
    Czlonkowie = new List<string>(wzor.Czlonkowie);   // kopia glaboka
}
Rodzaj kopiiCo powstajeKiedy wystarcza
płytkanowy obiekt, ale pola-obiekty wspólnegdy pola to liczby i napisy albo gdy współdzielenie jest zamierzone
głębokanowy obiekt i nowe kopie pól-obiektówgdy 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

Przykład: dwie nazwy, jedna szafka

Program.cs
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());
    }
}
wynik w konsoli
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

  • tenSam to druga nazwa tego samego zespołu — zmiana nazwy widoczna jest przez obie.
  • DodajCzlonka zmienia zawartość przekazanego obiektu, więc efekt widać po powrocie z metody.
  • PodmienZespol tworzy 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.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
typ wartościowyzmienna trzyma wartośćint, double, bool, struct — kopiowane przy przypisaniu.
typ referencyjnyzmienna trzyma wskazanieclass, tablica, List<T>, string.
b = a;kopia wskazaniaObiekt zostaje jeden; liczba obiektów to liczba wywołań new.
== dla klasporównanie tożsamościSprawdza, czy to ten sam obiekt, nie czy ma taką samą zawartość.
== dla stringporównanie zawartościWyjątek wbudowany w język.
parametr zwykłykopia wskazaniaWolno zmienić zawartość obiektu, nie wolno podmienić obiektu.
parametr refdostęp do zmiennejPozwala podmienić cały obiekt (lekcja 20).
new List<T>(inna)kopia kolekcjiPodstawowe narzędzie kopii głębokiej.
nullbrak wskazaniaUżycie kropki kończy się NullReferenceException (lekcja 34).
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Robot b = a; w celu skopiowaniaPowstaje druga nazwa, nie drugi obiekt. Kopia wymaga new.
Liczenie obiektów po zmiennychCztery zmienne mogą wskazywać dwa obiekty. Licz wywołania new.
p1 == p2 dla dwóch identycznych obiektówFalse — porównywana jest tożsamość. Do porównania zawartości nadpisz Equals (lekcja 51).
Oczekiwanie, że metoda podmieni obiektBez 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ściChroni 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

Zadania

ZAD 1Ile obiektów★☆☆

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.

ZAD 2Wartość kontra wskazanie★☆☆

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.

ZAD 3Podmiana bez skutku★★☆

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.

ZAD 4Porównania★★☆

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.

ZAD 5Płytka i głęboka★★☆

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

ZAD 6Kopia zagnieżdżona★★★

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

PODSUMOWANIE

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ść; string jest wyjątkiem.
  • Kopia płytka dzieli pola-obiekty z oryginałem, głęboka tworzy ich własne kopie.
  • readonly chroni wskazanie, nigdy zawartość kolekcji.

Dokumentacja: Microsoft Learn — temat tej lekcji.