Struktury — małe wartości, które kopiujemy

Pozycja pionka składa się z dwóch współrzędnych. Gdy kopiujesz pozycję, zwykle chcesz niezależną wartość, a nie drugą nazwę tego samego zmiennego obiektu.

CEL LEKCJI

Czego się dziś nauczysz

Po lekcji wyjaśnisz poznany mechanizm, prześledzisz działanie programu i zastosujesz go w samodzielnym zadaniu. Pracuj po kolei według spisu OOP. Potrzebujesz podstaw zmiennych, warunków, metod i wcześniejszych tematów tego modułu.

Zaplanuj 1–2 godziny lekcyjne, a zadania rozszerzające wykonaj w osobnym laboratorium. Używamy .NET 8 lub nowszego. Nie łącz kilku kompletnych przykładów w jednym Program.cs.

TEORIA

Wyjaśnienie krok po kroku

Struct opisuje typ wartościowy

Struktura może mieć pola, właściwości, metody i konstruktory. Przy zwykłym przypisaniu kopiujemy jej wartość. To istotna różnica wobec klasy, gdzie kopiowana jest referencja. Nie wybieramy struct tylko dlatego, że chcemy napisać mniej kodu albo „oszczędzić stos”.

Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.

Fragment C#
struct Rozmiar
{
    public int Szerokosc;
    public int Wysokosc;
}

Rozmiar grupuje dwie liczby w jedną wartość. Każde przypisanie kopiuje te pola.

Przed i po zmianie typu

Dla klasy z właściwością X zapis b = a i potem b.X = 9 zmienia obiekt widoczny przez a. Dla struktury z taką właściwością b = a kopiuje wartość i zmiana b nie zmienia a. Wyjątkiem od niezależności zawartości jest współdzielony obiekt wskazywany przez pole referencyjne wewnątrz struktury.

Wartość domyślna ma znaczenie

default(Punkt) ustawia pola struktury na wartości domyślne, np. współrzędne na 0. Elementy nowej tablicy struktur także zaczynają jako wartości domyślne. Projektuj typ tak, aby rozumieć tę sytuację. Sam konstruktor z walidacją nie blokuje powstania default.

Tak wygląda to w kodzie — Fragment wnętrza Main; Punkt ma publiczne pola X i Y. To fragment ilustracyjny, nie cały Program.cs.

Fragment C#
Punkt zero = default(Punkt);
Console.WriteLine(zero.X); // 0
Console.WriteLine(zero.Y); // 0

Default tworzy wartość z domyślnymi polami. WriteLine(int) wypisuje liczbę i kończy wiersz.

Konstruktory struktur

Możesz deklarować konstruktor z parametrami. Od C# 10 można także zdefiniować publiczny konstruktor bezparametrowy. Jawne new S() wywoła taki konstruktor, lecz default(S) i zerowanie elementów tablicy nie wykonają go. Nie przenoś z klasy reguły „własny konstruktor usuwa możliwość new bez argumentów” na strukturę.

Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.

Fragment C#
struct Ustawienia
{
    public int Proby;
    public Ustawienia() { Proby = 3; }
}
// new Ustawienia().Proby daje 3
// default(Ustawienia).Proby daje 0

Publiczny konstruktor bezparametrowy działa w C# 10 i nowszym. Default nie uruchamia tego konstruktora.

Readonly struct

Readonly struct pomaga projektować wartości niezmienne: pola instancji są readonly, a właściwości instancji nie mogą mieć zwykłych setterów zmieniających stan. Zamiast przesuwać istniejącą pozycję, metoda może zwrócić nową pozycję. To ułatwia śledzenie zmian. Nadal nie zamraża obiektów wskazywanych przez ewentualne pola referencyjne.

Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.

Fragment C#
readonly struct Punkt
{
    public int X { get; }
    public Punkt(int x) { X = x; }
    public Punkt Wprawo() { return new Punkt(X + 1); }
}

Wprawo zwraca nową wartość; nie zmienia bieżącej. Aby zachować wynik, klient przypisuje go do zmiennej.

Dziedziczenie, null i boxing

Struktura nie dziedziczy po wybranej klasie ani innej strukturze, ale może implementować interfejsy. Zwykły Punkt nie przechowuje null; Punkt? oznacza wartość opcjonalną. Przypisanie struktury do object lub zmiennej interfejsowej może wykonać boxing: opakować kopię wartości w obiekt. Na podstawowym poziomie nie potrzebujesz tego mechanizmu do przechowywania współrzędnych.

Uwaga na kopie w kolekcjach

Pobranie struktury z List<Punkt> daje wartość. Zmiana lokalnej kopii nie zapisze jej automatycznie na liście. Dla mutowalnych struktur konieczne bywa przypisanie zmienionej wartości z powrotem. Niezmienne małe struktury pozwalają wyraźnie zobaczyć tę operację.

PRZYKŁAD

Przewidź → uruchom → zmień

Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program uruchamiaj w oddzielnym projekcie konsolowym.

Program.cs
using System;
readonly struct Punkt
{
    public int X { get; }
    public int Y { get; }
    public Punkt(int x, int y) { X = x; Y = y; }
    public Punkt Wprawo() { return new Punkt(X + 1, Y); }
}
class Program
{
    static void Main()
    {
        Punkt a = new Punkt(2, 3);
        Punkt b = a;
        b = b.Wprawo();
        Punkt zero = default(Punkt);
        Console.WriteLine($"a: {a.X}, {a.Y}");
        Console.WriteLine($"b: {b.X}, {b.Y}");
        Console.WriteLine($"default: {zero.X}, {zero.Y}");
    }
}
Wynik przykładu
a: 2, 3
b: 3, 3
default: 0, 0
TEORIA

Co dokładnie robi kod?

B otrzymuje kopię wartości a. Wprawo nie zmienia tej wartości: tworzy i zwraca nowy Punkt. Przypisanie zapisuje wynik w b, a pozostaje bez zmian. Konstruktor ustawia automatyczne właściwości get-only. Default jest operatorem języka, nie metodą biblioteki. Console.WriteLine wypisuje tekst z interpolacji. Przykład zakłada małe współrzędne, bez przepełnienia int.

Zmień jedną daną i zapisz przewidywany wynik przed uruchomieniem. Następnie porównaj obserwację z przewidywaniem i wskaż instrukcję odpowiedzialną za różnicę.

Rekord to struktura, a unia?

Typ rekordowy z podstawy programowej to w C# właśnie struct — albo record w nowszym zapisie. Unii — typu, w którym wszystkie pola dzielą to samo miejsce w pamięci i naraz sensowne jest tylko jedno — C# nie ma jako osobnej konstrukcji. Da się ją odtworzyć atrybutem [StructLayout(LayoutKind.Explicit)] z jawnymi przesunięciami pól, ale robi się to wyłącznie przy współpracy z kodem systemowym. W zwykłym kodzie ten sam problem rozwiązuje się dziedziczeniem albo typem z polem opisującym wariant.

ZADANIA

Zadania

Nie ma tu gotowych rozwiązań. Oddaj kod, własne przypadki testowe oraz krótkie wyjaśnienie decyzji. Pierwsze zadanie jest podstawowe; ostatnie pozwalają pogłębić temat.

ZAD 1Ćwiczenie 1★☆☆

Zaprojektuj readonly struct Rozmiar z szerokością i wysokością oraz właściwością Pole. Zdecyduj, co oznacza default i opisz tę decyzję.

ZAD 2Ćwiczenie 2★☆☆

W dwóch osobnych programach utwórz mutowalny typ z X: raz class, raz struct. Wykonaj a → b, zmień b.X i porównaj odczyt a.X.

ZAD 3Ćwiczenie 3★★☆

Zbuduj strukturę zawierającą List<int>. Skopiuj ją, dopisz liczbę do listy kopii i wyjaśnij obserwację. Add(int) dopisuje element i zwraca void.

ZAD 4Ćwiczenie 4★★☆

Rozszerzenie: porównaj new i default dla struktury z jawnym konstruktorem bezparametrowym, który ustawia pole na 5. Sprawdź również element nowej tablicy.

PODSUMOWANIE

Sprawdź, czy rozumiesz

  • Co jest kopiowane przy przypisaniu struct?
  • Czy readonly zamraża obiekt przechowywany w polu?
  • Dlaczego default może ominąć twój konstruktor?

Dokumentacja: Microsoft Learn — Struktury.