Właściwości w C# – auto-property, init, required i pułapka private set
public class ZamowienieZle
{
public decimal Kwota; // publiczne pole — ktokolwiek może wstawić -500
}
zamowienie.Kwota = -500; // ❌ kompiluje się, nic tego nie powstrzymuje
Publiczne pole nie daje żadnej kontroli nad tym, co się do niego wstawia — ani teraz, ani w przyszłości, gdy zechcesz dodać walidację. Właściwości (properties) rozwiązują to, wyglądając z zewnątrz jak pole, a w środku będąc metodami get/set, które możesz kontrolować.
Auto-implemented property — najczęstszy przypadek
public class Zamowienie
{
public decimal Kwota { get; set; } // kompilator sam tworzy ukryte pole w tle
}
To wygląda jak pole, ale to właściwość — kompilator generuje niewidoczne, prywatne pole trzymające wartość, a { get; set; } to metody dostępu do niego. Z zewnątrz zamowienie.Kwota = 100; wygląda identycznie jak przy polu — różnica ujawnia się, gdy zaczniesz kontrolować, co dzieje się przy odczycie/zapisie.
Pełna właściwość z logiką w setterze
public class Zamowienie
{
private decimal _kwota;
public decimal Kwota
{
get => _kwota;
set
{
if (value < 0)
throw new ArgumentException("Kwota nie może być ujemna");
_kwota = value;
}
}
}
zamowienie.Kwota = -500; // ✅ rzuca ArgumentException — nie wejdzie w niepoprawnym stanie
Kod wywołujący nie widzi żadnej różnicy składniowej między auto-property a pełną właściwością — zamowienie.Kwota = 100 wygląda tak samo w obu przypadkach. To jest cała wartość properties: możesz zacząć od prostego auto-property, a dodać walidację później, bez zmiany kodu, który już z klasy korzysta.
get-only i init — właściwości tylko do odczytu
public class Zamowienie
{
public int Id { get; } // get-only — ustaw tylko w konstruktorze
public string Numer { get; init; } // init — ustaw w konstruktorze LUB inicjalizatorze obiektu
public Zamowienie(int id) => Id = id;
}
var z = new Zamowienie(5) { Numer = "ORD-001" }; // ✅ init pozwala na to
z.Id = 10; // ❌ błąd kompilacji — get-only, tylko konstruktor
z.Numer = "X"; // ❌ błąd kompilacji — init tylko przy tworzeniu, nie później
{ get; } (bez settera) pozwala ustawić wartość tylko w konstruktorze. { get; init; } (C# 9+) jest bardziej elastyczny — pozwala ustawić wartość też przez inicjalizator obiektowy ({ Numer = "..." }), ale tylko podczas tworzenia obiektu. Po zakończeniu inicjalizacji obie działają identycznie: wartości nie da się już zmienić.
Pułapka: private set kontra init — to NIE to samo
public class ZWlasnymSetterem
{
public int Wartosc { get; private set; }
public void Zmien(int nowa) => Wartosc = nowa; // ✅ działa — metoda klasy ma dostęp do private set
}
public class ZInit
{
public int Wartosc { get; init; }
public void Zmien(int nowa) => Wartosc = nowa; // ❌ błąd kompilacji — init nie pozwala na to NIGDY po konstrukcji, nawet z wnętrza klasy!
}
To rozróżnienie umyka nawet doświadczonym programistom: private set pozwala metodom tej samej klasy zmieniać wartość w dowolnym momencie życia obiektu — tylko świat zewnętrzny jest zablokowany. init blokuje zmianę zupełnie, także dla kodu wewnątrz klasy, natychmiast po zakończeniu konstrukcji obiektu. Wybór między nimi to deklaracja intencji: private set = “mogę to zmienić wewnętrznie w reakcji na coś”, init = “to jest naprawdę niezmienne po utworzeniu, kropka”.
required — wymuszenie ustawienia właściwości
public class Zamowienie
{
public required int KlientId { get; init; }
}
var z1 = new Zamowienie(); // ❌ błąd kompilacji — KlientId nie ustawione
var z2 = new Zamowienie { KlientId = 5 }; // ✅
required (C# 11) wymusza ustawienie właściwości przez inicjalizator obiektowy, bez pisania osobnego konstruktora — kompilator odmówi skompilowania kodu, który tworzy obiekt bez podania wymaganej wartości. Szerzej opisane razem z konstruktorami w Konstruktory w C#.
Właściwość obliczana (expression-bodied)
public class Prostokat
{
public double Szerokosc { get; init; }
public double Wysokosc { get; init; }
public double Pole => Szerokosc * Wysokosc; // liczona przy KAŻDYM odczycie, nie przechowywana
}
Pole nie ma pola w tle — jej wartość jest przeliczana na nowo za każdym razem, gdy ktoś ją odczyta. To idealne dla wartości pochodnych od innych właściwości, które muszą zawsze być aktualne.
Pułapka: getter ze skutkami ubocznymi
public class ZlyPrzyklad
{
private int _liczbaOdczytow;
public string Status
{
get
{
_liczbaOdczytow++; // ❌ efekt uboczny w getterze — zaskakujące i nieoczywiste
LogujDoPliku("Odczytano status"); // ❌ jeszcze gorzej — I/O w getterze
return "Aktywny";
}
}
}
Wywołujący kod widzi zamowienie.Status i zakłada, że to tani, bezpieczny odczyt — jak przy zwykłym polu. Getter, który loguje, liczy, zapisuje do bazy albo wykonuje inne efekty uboczne, łamie to założenie w sposób, którego nic w składni nie sygnalizuje. Zasada: gettery powinny być tanie i wolne od efektów ubocznych — jeśli operacja jest kosztowna albo coś zmienia, powinna być zwykłą metodą (PobierzStatus()), nie właściwością, żeby wywołujący widział, że to coś więcej niż odczyt pola.
Właściwości w interfejsach
public interface IProdukt
{
string Nazwa { get; } // tylko sygnatura — implementująca klasa decyduje, skąd bierze wartość
decimal Cena { get; set; }
}
public class Produkt : IProdukt
{
public string Nazwa { get; init; } = "";
public decimal Cena { get; set; }
}
Interfejs może wymagać właściwości, ale — tak jak przy metodach — nie może mieć za nią pola ani logiki. Implementująca klasa sama decyduje, czy Nazwa to auto-property, właściwość obliczana, czy coś czytającego z bazy danych — interfejs wymusza tylko kontrakt “musisz to udostępnić”, nie sposób realizacji (więcej o różnicy interfejs/klasa w Klasa abstrakcyjna vs interfejs).
Właściwości w record — przypomnienie
Pozycyjny record Osoba(string Imie) automatycznie tworzy właściwość public string Imie { get; init; } — to dokładnie ten sam mechanizm co tutaj, tylko wygenerowany przez kompilator z sygnatury konstruktora. Szczegóły w class vs struct vs record.
Podsumowanie
Właściwości wyglądają z zewnątrz jak pola, ale dają kontrolę, której pola nie mają — od prostego auto-property, przez walidację w setterze, po pełną niemutowalność przez init. Najważniejsze rozróżnienie do zapamiętania: private set wciąż pozwala klasie zmieniać wartość wewnętrznie, init blokuje to na zawsze po konstrukcji — dobór złego z nich to częsty, subtelny błąd projektowy. Trzymaj gettery tanie i bez efektów ubocznych — jeśli operacja robi więcej niż zwraca wartość, powinna być metodą.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →