🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

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

Dodaj komentarz

czytanie to początek

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ę →