Referencje w C# – jak obiekty żyją w pamięci
Zrozumiesz różnicę między typami wartościowymi a referencyjnymi, nauczysz się unikać NullReferenceException i dowiesz się, dlaczego kopiowanie obiektów nie działa tak jak myślisz!
Problem – dziwne zachowanie obiektów
Zanim wyjaśnimy czym jest referencja, zobaczmy dziwne zachowanie, które zaskakuje każdego początkującego programistę.
public class Osoba { public string Imie; public int Wiek; } // Tworzę osobę Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; osoba1.Wiek = 25; // "Kopiuję" do osoba2 Osoba osoba2 = osoba1; // Zmieniam TYLKO osoba2 osoba2.Imie = "Anna"; osoba2.Wiek = 30; // Co się wyświetli? Console.WriteLine(osoba1.Imie); // 🤔 ??? Console.WriteLine(osoba1.Wiek); // 🤔 ???
Wynik to:
Anna 30
Ale przecież zmieniałem tylko osoba2! Dlaczego osoba1 też się zmieniła?
Porównajmy to z zachowaniem liczb:
// Z liczbami działa "normalnie" int liczba1 = 10; int liczba2 = liczba1; // Kopia liczba2 = 99; // Zmieniam tylko liczba2 Console.WriteLine(liczba1); // 10 ✅ Bez zmian! Console.WriteLine(liczba2); // 99
Dlaczego liczby zachowują się „normalnie”, a obiekty „dziwnie”? Odpowiedź: referencje.
Czym jest referencja?
Referencja to „adres” obiektu w pamięci komputera. Gdy tworzysz obiekt, zmienna nie przechowuje samego obiektu – przechowuje tylko informację o tym, gdzie ten obiekt się znajduje.
Wyobraź sobie, że masz notes z adresami znajomych:
- Dom znajomego = obiekt w pamięci (prawdziwe dane)
- Adres w notesie = referencja (wskazówka gdzie szukać)
- Kartka w notesie = zmienna
Gdy „kopiujesz” adres do innego notesu, nie budujesz drugiego domu – masz tylko dwa adresy prowadzące do tego samego miejsca!
Inna analogia:
- Telewizor = obiekt (prawdziwe urządzenie)
- Pilot = referencja (sposób kontrolowania telewizora)
Możesz mieć dwa piloty do tego samego telewizora. Gdy użyjesz jednego pilota aby zmienić głośność, drugi pilot też „widzi” zmianę – bo oba kontrolują ten sam telewizor!
(przechowuje adres)
Wiek: 25
adres: 0x1A2B
↑ Zmienna osoba1 zawiera adres (np. 0x1A2B), nie sam obiekt
Typy wartościowe (value types)
Typy wartościowe przechowują swoją wartość bezpośrednio w zmiennej. Gdy kopiujesz zmienną, kopiujesz samą wartość.
Typy wartościowe w C#
| Typ | Przykład | Opis |
|---|---|---|
int | int x = 42; | Liczba całkowita |
double | double y = 3.14; | Liczba zmiennoprzecinkowa |
bool | bool ok = true; | Wartość logiczna |
char | char c = 'A'; | Pojedynczy znak |
decimal | decimal m = 9.99m; | Precyzyjne obliczenia |
struct | DateTime, Point | Struktury (wartościowe) |
// Typy wartościowe – kopiowanie WARTOŚCI int a = 10; int b = a; // Kopiujemy WARTOŚĆ 10 b = 99; // Zmieniamy tylko b Console.WriteLine(a); // 10 ✅ Bez zmian! Console.WriteLine(b); // 99 // To samo z innymi typami wartościowymi: double x = 3.14; double y = x; y = 2.71; Console.WriteLine(x); // 3.14 ✅ bool flaga1 = true; bool flaga2 = flaga1; flaga2 = false; Console.WriteLine(flaga1); // True ✅
↑ Każda zmienna ma swoją WŁASNĄ kopię wartości
Typy wartościowe to jak pudełka z przedmiotami w środku. Gdy „kopiujesz” pudełko, dostajesz nowe pudełko z kopią przedmiotu. Zmiana w jednym pudełku nie wpływa na drugie.
Typy referencyjne (reference types)
Typy referencyjne przechowują adres (referencję) do obiektu w pamięci. Gdy kopiujesz zmienną, kopiujesz tylko adres, nie sam obiekt!
Typy referencyjne w C#
| Typ | Przykład | Opis |
|---|---|---|
class | new Osoba() | Wszystkie klasy |
string | "Hello" | Łańcuchy znaków |
array | new int[5] | Tablice |
object | new object() | Klasa bazowa |
interface | IList<int> | Interfejsy |
delegate | Action, Func | Delegaty |
public class Osoba { public string Imie; public int Wiek; } // Tworzymy obiekt Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; osoba1.Wiek = 25; // "Kopiujemy" – ale tylko ADRES! Osoba osoba2 = osoba1; // Kopia REFERENCJI, nie obiektu! // Zmieniamy przez osoba2 osoba2.Imie = "Anna"; osoba2.Wiek = 30; // OBE zmienne wskazują na TEN SAM obiekt! Console.WriteLine(osoba1.Imie); // Anna 😱 Console.WriteLine(osoba1.Wiek); // 30 😱 Console.WriteLine(osoba2.Imie); // Anna Console.WriteLine(osoba2.Wiek); // 30
Wiek: 30
↑ Obie zmienne mają TEN SAM adres – wskazują na jeden obiekt!
Osoba osoba2 = osoba1; NIE tworzy kopii obiektu!
To tylko kopiuje adres – teraz masz dwie zmienne wskazujące na ten sam obiekt. Zmiana przez jedną zmienną jest widoczna przez drugą.
Wizualizacja w pamięci
Zobaczmy jak wyglądają typy wartościowe i referencyjne w pamięci komputera.
Typy wartościowe – każda zmienna ma własną kopię
int a = 10; int b = a; b = 99; PAMIĘĆ (STOS): ┌─────────────┐ │ a: 10 │ ← Osobna komórka ├─────────────┤ │ b: 99 │ ← Osobna komórka └─────────────┘ Zmiana b NIE wpływa na a!
Typy referencyjne – zmienne dzielą ten sam obiekt
Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; Osoba osoba2 = osoba1; osoba2.Imie = "Anna"; PAMIĘĆ: STOS (zmienne): STERTA (obiekty): ┌─────────────────┐ ┌─────────────────┐ │ osoba1: 0x1A2B ─┼─────────→│ Obiekt Osoba │ ├─────────────────┤ │ Imie: "Anna" │ │ osoba2: 0x1A2B ─┼─────────→│ Wiek: 25 │ └─────────────────┘ └─────────────────┘ Obie zmienne wskazują na TEN SAM obiekt!
Tworzenie nowych obiektów – różne adresy
// Każdy NEW tworzy NOWY obiekt! Osoba osoba1 = new Osoba(); // Nowy obiekt (adres: 0x1A2B) osoba1.Imie = "Jan"; Osoba osoba2 = new Osoba(); // INNY nowy obiekt (adres: 0x3C4D) osoba2.Imie = "Anna"; // Teraz to SĄ RÓŻNE obiekty! osoba2.Imie = "Maria"; Console.WriteLine(osoba1.Imie); // Jan ✅ Bez zmian! Console.WriteLine(osoba2.Imie); // Maria
PAMIĘĆ:
STOS: STERTA:
┌─────────────────┐ ┌─────────────────┐
│ osoba1: 0x1A2B ─┼─────────→│ Obiekt Osoba │
├─────────────────┤ │ Imie: "Jan" │
│ osoba2: 0x3C4D ─┼───┐ └─────────────────┘
└─────────────────┘ │
│ ┌─────────────────┐
└─────→│ Obiekt Osoba │
│ Imie: "Maria" │
└─────────────────┘
Różne adresy = różne obiekty!
Każde użycie new tworzy nowy obiekt z nowym adresem. Bez new – tylko kopiujesz adres istniejącego obiektu.
Porównanie: wartość vs referencja
Typy wartościowe
- Przechowują: samą wartość
- Kopiowanie: kopia wartości
- Zmiana kopii: NIE wpływa na oryginał
- Przykłady: int, double, bool, char, struct
- Domyślna wartość: 0, false, '\0′
Typy referencyjne
- Przechowują: adres obiektu
- Kopiowanie: kopia adresu
- Zmiana „kopii”: WPŁYWA na „oryginał”
- Przykłady: class, string, array
- Domyślna wartość: null
| Cecha | Wartościowe | Referencyjne |
|---|---|---|
| Co w zmiennej? | Sama wartość | Adres (referencja) |
| Gdzie w pamięci? | Stos (stack) | Sterta (heap) |
| Czy może być null? | Nie (chyba że nullable) | Tak |
| Porównanie == | Porównuje wartości | Porównuje adresy* |
* String jest wyjątkiem – porównuje zawartość, nie adresy.
null – brak obiektu
null oznacza „brak referencji” – zmienna nie wskazuje na żaden obiekt. To jak adres w notesie, który jest pusty.
// Deklaracja bez inicjalizacji – domyślnie null Osoba osoba1; // null (niezainicjalizowana) // Jawne przypisanie null Osoba osoba2 = null; // Jawnie: "nie ma obiektu" // Po użyciu obiektu – "zapominamy" o nim Osoba osoba3 = new Osoba(); osoba3.Imie = "Jan"; osoba3 = null; // Teraz osoba3 już nie wskazuje na obiekt
null to jak pusta strona w notesie z adresami:
- Strona istnieje (zmienna jest zadeklarowana)
- Ale nie ma na niej żadnego adresu (nie wskazuje na obiekt)
- Nie możesz „pójść” pod pusty adres!
(brak adresu)
NullReferenceException – najczęstszy błąd!
Gdy próbujesz użyć obiektu przez zmienną, która jest null, dostajesz NullReferenceException – jeden z najczęstszych błędów w C#!
Osoba osoba = null; // ❌ BŁĄD! Próba użycia null osoba.Imie = "Jan"; // NullReferenceException! // Komunikat błędu: // System.NullReferenceException: Object reference not set to an instance of an object.
„Próbujesz użyć obiektu, ale zmienna nie wskazuje na żaden obiekt (jest null).”
To jak próba zadzwonienia pod pusty numer telefonu – nie ma komu odebrać!
Jak uniknąć NullReferenceException?
Osoba osoba = null; // Sposób 1: Sprawdzenie != null if (osoba != null) { osoba.Imie = "Jan"; // Bezpieczne! } else { Console.WriteLine("Brak osoby!"); } // Sposób 2: Operator ?. (null-conditional) string imie = osoba?.Imie; // Zwróci null jeśli osoba jest null // Sposób 3: Operator ?? (null-coalescing) string imie2 = osoba?.Imie ?? "Nieznane"; // Domyślna wartość // Sposób 4: Pattern matching (C# 7+) if (osoba is not null) { Console.WriteLine(osoba.Imie); }
| Operator | Składnia | Co robi? |
|---|---|---|
!= null | if (x != null) | Klasyczne sprawdzenie |
?. | x?.Pole | Zwraca null jeśli x jest null |
?? | x ?? domyslna | Wartość domyślna gdy null |
is not null | if (x is not null) | Pattern matching |
Sprawdzanie referencji
Możesz sprawdzić, czy dwie zmienne wskazują na ten sam obiekt (tę samą referencję).
Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; Osoba osoba2 = new Osoba(); // NOWY obiekt! osoba2.Imie = "Jan"; Osoba osoba3 = osoba1; // Kopia REFERENCJI // Sprawdzanie czy to TEN SAM obiekt (ta sama referencja) Console.WriteLine(osoba1 == osoba2); // False – różne obiekty! Console.WriteLine(osoba1 == osoba3); // True – ten sam obiekt! // ReferenceEquals – jawne sprawdzenie referencji Console.WriteLine(ReferenceEquals(osoba1, osoba2)); // False Console.WriteLine(ReferenceEquals(osoba1, osoba3)); // True // Sprawdzenie czy obiekt istnieje Console.WriteLine(osoba1 != null); // True
Dla większości klas, operator == porównuje referencje (czy to ten sam obiekt), nie zawartość!
Wyjątek: string – operator == porównuje zawartość tekstu.
Tablice są referencyjne!
Tablice w C# to typy referencyjne – kopiowanie zmiennej tablicowej kopiuje tylko referencję, nie elementy!
int[] tablica1 = { 1, 2, 3 }; // "Kopiujemy" tablicę – ale to tylko kopia referencji! int[] tablica2 = tablica1; // Zmieniamy przez tablica2 tablica2[0] = 999; // OBE tablice pokazują zmianę! Console.WriteLine(tablica1[0]); // 999 😱 Console.WriteLine(tablica2[0]); // 999 // Czy to ten sam obiekt? Console.WriteLine(tablica1 == tablica2); // True
Jak NAPRAWDĘ skopiować tablicę?
int[] original = { 1, 2, 3 }; // Sposób 1: Metoda Clone() int[] kopia1 = (int[])original.Clone(); // Sposób 2: Array.Copy() int[] kopia2 = new int[original.Length]; Array.Copy(original, kopia2, original.Length); // Sposób 3: ToArray() z LINQ int[] kopia3 = original.ToArray(); // Teraz zmiany w kopii nie wpływają na oryginał! kopia1[0] = 999; Console.WriteLine(original[0]); // 1 ✅
String – specjalny przypadek
string jest typem referencyjnym, ale zachowuje się trochę jak typ wartościowy dzięki niemutowalności (immutability).
string tekst1 = "Hello"; string tekst2 = tekst1; // Kopia referencji // Ale zmiana "tekst2" nie wpływa na tekst1! tekst2 = "World"; // To tworzy NOWY obiekt string! Console.WriteLine(tekst1); // "Hello" ✅ Console.WriteLine(tekst2); // "World" // Porównanie == dla stringów porównuje ZAWARTOŚĆ string a = "Test"; string b = "Test"; Console.WriteLine(a == b); // True – ta sama zawartość!
String jest niemutowalny (immutable) – nie możesz zmienić jego zawartości. Każda „modyfikacja” tworzy nowy obiekt:
string s = "Hello";
s = s + " World"; // Tworzy NOWY string "Hello World"
// Stary "Hello" pozostaje niezmieniony
Dlatego tekst2 = "World" nie zmienia oryginalnego stringa – tworzy nowy i przypisuje do tekst2 nową referencję.
Przekazywanie do metod
Gdy przekazujesz obiekt do metody, przekazujesz kopię referencji. Metoda może zmienić zawartość obiektu!
public class Osoba { public string Imie; public int Wiek; } // Metoda przyjmująca obiekt static void ZmienWiek(Osoba osoba) { osoba.Wiek = 100; // Zmienia ORYGINALNY obiekt! } // Użycie Osoba jan = new Osoba(); jan.Imie = "Jan"; jan.Wiek = 25; ZmienWiek(jan); Console.WriteLine(jan.Wiek); // 100 😱 Zmienione!
Porównanie: wartość vs referencja w metodach
// Typ wartościowy – kopia wartości static void ZmienLiczbe(int x) { x = 999; // Zmienia tylko LOKALNĄ kopię } int liczba = 10; ZmienLiczbe(liczba); Console.WriteLine(liczba); // 10 ✅ Bez zmian! // Typ referencyjny – kopia referencji (ten sam obiekt) static void ZmienTablice(int[] tab) { tab[0] = 999; // Zmienia ORYGINALNĄ tablicę! } int[] tablica = { 1, 2, 3 }; ZmienTablice(tablica); Console.WriteLine(tablica[0]); // 999 😱 Zmienione!
Gdy przekazujesz obiekt lub tablicę do metody, metoda może modyfikować zawartość! To często pożądane zachowanie, ale może też prowadzić do błędów jeśli o tym zapomnisz.
Częste błędy
❌ Błąd 1: Myślenie że = kopiuje obiekt
❌ Źle rozumiane
Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; Osoba osoba2 = osoba1; // "Teraz mam dwa obiekty!" // NIE! To ten sam obiekt!
✅ Prawda
Osoba osoba1 = new Osoba(); osoba1.Imie = "Jan"; Osoba osoba2 = osoba1; // osoba1 i osoba2 wskazują // na TEN SAM obiekt!
❌ Błąd 2: Nie sprawdzanie null
❌ Źle
Osoba osoba = null; osoba.Imie = "Jan"; // NullReferenceException!
✅ Dobrze
Osoba osoba = null;
if (osoba != null)
{
osoba.Imie = "Jan";
}
❌ Błąd 3: Myślenie że tablice kopiują elementy
❌ Źle
int[] tab1 = {1, 2, 3};
int[] tab2 = tab1;
// "tab2 to kopia!"
// NIE! To ta sama tablica!
✅ Dobrze
int[] tab1 = {1, 2, 3};
int[] tab2 = (int[])tab1.Clone();
// Teraz tab2 to PRAWDZIWA
// kopia tablicy!
❌ Błąd 4: Porównywanie obiektów przez ==
❌ Mylące
Osoba o1 = new Osoba(); o1.Imie = "Jan"; Osoba o2 = new Osoba(); o2.Imie = "Jan"; o1 == o2 // False! // Różne obiekty mimo // tej samej zawartości
✅ Świadome
// == porównuje REFERENCJE // (czy to ten sam obiekt) // Dla porównania zawartości // użyj metody Equals() // lub zdefiniuj własną
Podsumowanie
- Referencja – „adres” obiektu w pamięci
- Typ wartościowy – przechowuje wartość bezpośrednio (int, double, bool)
- Typ referencyjny – przechowuje adres obiektu (class, array, string)
- null – brak referencji (nie wskazuje na żaden obiekt)
- NullReferenceException – próba użycia null
Porównanie typów
| Wartościowe | Referencyjne | |
|---|---|---|
| W zmiennej | Sama wartość | Adres obiektu |
| Kopiowanie | Kopia wartości | Kopia adresu |
| Zmiana kopii | Nie wpływa | Wpływa na oryginał! |
| Może być null? | Nie | Tak |
| Przykłady | int, double, bool | class, array, string |
Zasady do zapamiętania
// 1. new tworzy NOWY obiekt Osoba a = new Osoba(); // Nowy obiekt Osoba b = new Osoba(); // INNY nowy obiekt // 2. = kopiuje REFERENCJĘ, nie obiekt Osoba c = a; // c i a wskazują na TEN SAM obiekt // 3. Zawsze sprawdzaj null przed użyciem if (osoba != null) { /* bezpiecznie */ } // 4. Tablice to typy referencyjne! int[] kopia = (int[])original.Clone(); // Prawdziwa kopia
Zadania praktyczne
📝 Zadanie 1: Przewiduj wynik
Co wyświetli się na ekranie? Odpowiedz BEZ uruchamiania kodu, potem sprawdź.
public class Ksiazka
{
public string Tytul;
public int Strony;
}
Ksiazka ksiazka1 = new Ksiazka();
ksiazka1.Tytul = "Hobbit";
ksiazka1.Strony = 300;
Ksiazka ksiazka2 = ksiazka1;
ksiazka2.Strony = 350;
Console.WriteLine(ksiazka1.Strony); // ???
💡 Podpowiedź: Czy ksiazka2 to kopia obiektu czy kopia referencji?
📝 Zadanie 2: Znajdź błąd
Ten kod rzuca wyjątek. Znajdź błąd i napraw go.
public class Student
{
public string Imie;
public double Ocena;
}
Student student = null;
student.Imie = "Kowalski";
💡 Podpowiedź: Co oznacza null?
📝 Zadanie 3: Ile obiektów?
Ile obiektów klasy Auto powstaje w tym kodzie?
Auto auto1 = new Auto(); Auto auto2 = new Auto(); Auto auto3 = auto1; Auto auto4 = auto2; Auto auto5 = auto3;
💡 Podpowiedź: Policz słowa new
⭐ Zadanie 4: Tablice
Napisz kod, który:
- Tworzy tablicę liczb {1, 2, 3, 4, 5}
- Tworzy PRAWDZIWĄ kopię tej tablicy
- Zmienia pierwszy element kopii na 999
- Wyświetla pierwszy element oryginału (powinien być nadal 1)
💡 Podpowiedź: Użyj Clone() lub Array.Copy()
⭐⭐ Zadanie 5: Bezpieczna metoda
Napisz metodę BezpiecznieWyswietl(Osoba osoba), która:
- Sprawdza czy osoba nie jest null
- Jeśli nie jest null – wyświetla imię i wiek
- Jeśli jest null – wyświetla „Brak danych”
Przetestuj metodę z obiektem i z null.
💡 Podpowiedź: Użyj if (osoba != null) lub operatora ?.