Menu

Właściwości w C#: get, set, auto-properties, init i required

Jak działają właściwości w C#: akcesory get i set nad polem pomocniczym, właściwości automatyczne, private set i właściwości tylko do odczytu, właściwości obliczane, walidacja w setterach oraz słowa kluczowe init i required z C# 9 i 11.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Właściwość wygląda dla używającego ją kodu jak pole (order.Total, order.Total = 5), ale w rzeczywistości jest parą metod: akcesora get, który zwraca wartość, i akcesora set, który ją przyjmuje. Ta pośredniość pozwala klasie sprawdzać wartości, obliczać je albo odmawiać zapisu, bez zmiany sposobu, w jaki korzystają z niej wywołujący.

Pola a właściwości

Publiczne pole daje bezpośredni dostęp do pamięci obiektu. Każdy może wpisać tam cokolwiek:

public class Account
{
    public decimal Balance;   // any code can write -500 here
}

Właściwość umieszcza kod między wywołującym a pamięcią. Oto pełna, ręcznie napisana forma: prywatne pole pomocnicze (backing field) i właściwość z oboma akcesorami. Wewnątrz set słowo kluczowe value to przypisywana wartość.

Wynik:

250
Rejected a negative balance
250

Poza walidacją właściwości mają znaczenie dlatego, że większość ekosystemu .NET pracuje z nimi i ignoruje pola: interfejsy mogą deklarować właściwości, ale nie pola instancji, data binding w WPF i MAUI wiąże się z właściwościami, a System.Text.Json domyślnie serializuje publiczne właściwości i pomija pola.

Właściwości automatyczne

Gdy akcesory miałyby tylko czytać i zapisywać pole, pozwól kompilatorowi je napisać. { get; set; } to właściwość implementowana automatycznie (auto-property): kompilator generuje za ciebie ukryte pole pomocnicze.

Wynik:

Notebook: 9.99, 10 left

Wygląda jak pole, więc po co się trudzić? Bo późniejsza zamiana publicznego pola na właściwość łamie zgodność ze skompilowanym kodem, który go używa (oraz z argumentami ref i out), a zamiana właściwości automatycznej na pełną właściwość z walidacją jest dla wywołujących niewidoczna. Zaczynając od { get; set; }, zachowujesz tę możliwość za darmo.

private set i właściwości tylko do odczytu

Większość stanu powinna być czytelna dla wszystkich, a zmieniana tylko przez sam obiekt. Nadaj setterowi węższy modyfikator dostępu:

Wynik:

Ines: 2 items, total 15.75

Mamy tu trzy rodzaje "tylko do odczytu":

  • { get; private set; }: klasa może ją zmienić w dowolnej chwili, kod zewnętrzny nie może.
  • { get; } (właściwość automatyczna tylko z getterem, C# 6): można ją przypisać tylko w konstruktorze albo w inicjalizatorze. Po utworzeniu obiektu nikt jej nie zmieni, nawet sama klasa. Tak tworzy się niemodyfikowalną właściwość.
  • => expression (właściwość obliczana): bez żadnej pamięci. Wyrażenie wykonuje się przy każdym odczycie, więc Total jest zawsze aktualne względem listy.

Właściwość obliczana powinna być tania i bez efektów ubocznych, bo wywołujący czytają właściwości bez zastanowienia, w pętlach i w debuggerze. Jeśli pobranie wartości wymaga prawdziwej pracy (zapytania do bazy, dużych obliczeń), zrób z tego metodę, na przykład CalculateTotal(), żeby koszt był widoczny.

Akcesory z ciałem wyrażeniowym

Pełne właściwości z jednolinijkowymi akcesorami mogą używać => dla każdego akcesora:

private string title;

public string Title
{
    get => title;
    set => title = value?.Trim() ?? "";
}

public decimal Total => SumPrices(); to skrót od właściwości tylko z getterem; get => ... to ten sam pomysł wewnątrz właściwości, która ma też setter.

Logika w getterze i setterze: powiadomienia i wartości leniwe

Ponieważ akcesory są metodami, mogą robić więcej niż tylko przechowywać wartość. Dwa częste wzorce: zgłaszanie powiadomienia o zmianie z settera oraz obliczanie wartości przy pierwszym odczycie i zapamiętywanie jej.

Wynik:

(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark

Changed to zdarzenie, a ten wzorzec settera to dokładnie to, jak wyglądają implementacje INotifyPropertyChanged w WPF i MAUI. nameof(Theme) utrzymuje string w zgodzie z nazwą, jeśli właściwość zostanie przemianowana.

Błąd nieskończonej rekurencji

Najczęstszy błąd z właściwościami to setter, który przypisuje do właściwości zamiast do pola pomocniczego:

public string Name
{
    get { return Name; }        // calls get again, forever
    set { Name = value; }       // calls set again, forever
}

Każdy akcesor wywołuje sam siebie, a program pada z StackOverflowException, którego nie da się przechwycić. Rozwiązaniem jest czytanie i zapisywanie osobnego pola (name, małą literą) albo użycie właściwości automatycznej. C# 14 dodaje słowo kluczowe field właśnie na tę okoliczność: wewnątrz akcesora field odnosi się do pola pomocniczego wygenerowanego przez kompilator, więc set => field = value.Trim(); działa bez deklarowania go.

Akcesory init (C# 9)

Właściwość tylko z getterem zmusza do przekazywania każdej wartości przez konstruktor. C# 9 dodał init, setter dozwolony tylko podczas tworzenia obiektu: w konstruktorze albo w inicjalizatorze obiektu.

public class Product
{
    public string Sku { get; init; }
    public decimal Price { get; init; }
}

var p = new Product { Sku = "MUG-01", Price = 8.50m };   // fine: during creation
p.Price = 4m;   // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
                // in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

W rezultacie dostajesz niemodyfikowalny obiekt ze składnią inicjalizatora, i właśnie tego używają klasy rekordów dla swoich właściwości pozycyjnych. Żeby "zmienić" taki obiekt, utwórz nowy; rekordy robią to w jednej linii za pomocą with.

W C# 7 najbliższym odpowiednikiem jest właściwość tylko z getterem i parametr konstruktora:

public class Product
{
    public string Sku { get; }
    public decimal Price { get; }
    public Product(string sku, decimal price) { Sku = sku; Price = price; }
}

Składowe required (C# 11)

Inicjalizator obiektu nie może zmusić wywołujących do ustawienia czegokolwiek: new Product { } kompiluje się nawet wtedy, gdy Sku nigdy nie może być puste. C# 11 dodał modyfikator required:

public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" };   // error CS9035: Required member 'User.Email'
                                              // must be set in the object initializer or attribute constructor.

required działa z set i init. Konstruktor, który sam ustawia wymagane składowe, może to zadeklarować atrybutem [SetsRequiredMembers], dzięki czemu wywołujący ten konstruktor nie muszą ustawiać ich ponownie.

Typowe błędy

  • Rekurencja w akcesorze. set { Name = value; } wywołuje sam siebie. Użyj pola pomocniczego albo właściwości automatycznej.
  • Kosztowne gettery lub gettery z efektami ubocznymi. Czytający oczekują, że odczyt właściwości będzie szybki i bezpieczny do powtarzania. Do pracy używaj metody.
  • Publiczne pola "na razie". Późniejsza zamiana pola na właściwość łamie zgodność binarną, a serializatory domyślnie ignorują pola. Zacznij od właściwości automatycznej.
  • Publiczne settery wszędzie. { get; set; } na saldzie albo statusie zaprasza nieprawidłowe stany. Użyj private set, init albo samego gettera, a stan zmieniaj przez metody.
  • Zwracanie modyfikowalnej kolekcji z właściwości tylko z getterem. public List<string> Tags { get; } powstrzymuje wywołujących przed podmianą listy, ale nie przed dodawaniem do niej. Zwracaj IReadOnlyList<string>, gdy kolekcja nie powinna się zmieniać z zewnątrz.

Najczęściej zadawane pytania

Co oznacza { get; set; } w C#?

Deklaruje właściwość automatyczną: kompilator tworzy ukryte prywatne pole, akcesor get, który je zwraca, i akcesor set, który je przypisuje. public string Name { get; set; } zachowuje się dla wywołujących jak publiczne pole, ale jest parą metod, więc możesz później dodać walidację albo zmienić ją na private set bez zmiany kodu, który jej używa.

Jaka jest różnica między polem a właściwością w C#?

Pole to zmienna przechowywana w obiekcie. Właściwość to para metod (get i set), która z zewnątrz wygląda jak pole. Właściwości pozwalają klasie walidować wartości, obliczać wyniki albo ograniczać zapis i to z nimi pracują data binding, serializatory i interfejsy. Publiczne dane udostępniaj jako właściwości; pola zwykle pozostają prywatne.

Czym jest private set w C#?

public int Stock { get; private set; } pozwala dowolnemu kodowi odczytać właściwość, ale zmienić ją może tylko kod wewnątrz klasy. W ten sposób klasa udostępnia stan, którego jest właścicielem, na przykład saldo albo licznik, a wszystkie zmiany trzyma za metodami, które pilnują zasad.

Co robi init w C#?

init (C# 9) to setter, który działa tylko podczas tworzenia obiektu: w konstruktorze albo w inicjalizatorze obiektu. public string Sku { get; init; } pozwala na new Product { Sku = "A-1" }, ale późniejsze product.Sku = "B-2" odrzuca błędem CS8852. Daje niemodyfikowalnym obiektom składnię inicjalizatora.

Czym jest właściwość required w C#?

Właściwość oznaczona required (C# 11) musi zostać ustawiona w każdym inicjalizatorze obiektu, który tworzy ten typ; pominięcie jej to błąd kompilacji (CS9035). Dobrze łączy się z init: public required string Email { get; init; } trzeba podać przy tworzeniu i nie można jej później zmienić.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ