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

const vs readonly vs static readonly w C#

Wyobraź sobie taką sytuację: masz bibliotekę Shared.dll, w której ktoś zdefiniował public const int MaxRetries = 3;. Pół roku później zmieniasz tę wartość na 5, publikujesz nową wersję biblioteki, podmieniasz plik .dll na serwerze — a aplikacja dalej robi tylko 3 próby. Żadnego błędu, żadnego wyjątku. Kod po prostu udaje, że nic się nie zmieniło.

To nie jest bug w .NET. To efekt uboczny tego, jak działa const — i jeśli nie znasz różnicy między const, readonly a static readonly, prędzej czy później to Ciebie ta różnica zaskoczy, i to zwykle w najgorszym możliwym momencie: na produkcji.

W tym artykule rozłożymy te trzy słowa kluczowe na czynniki pierwsze — nie na poziomie “co piszę w kodzie”, tylko “co się faktycznie dzieje w pamięci i w skompilowanym IL”.

Czym jest const?

const to stała czasu kompilacji (compile-time constant). Oznacza to, że wartość musi być znana już w momencie, gdy kompilator przetwarza Twój kod — nie może pochodzić z bazy danych, pliku konfiguracyjnego, argumentu metody ani nawet z wyniku innej metody.

public const int MaxRetries = 3;
public const string AppName = "DevHobby";
public const double Pi = 3.14159;

Ograniczenia const są dość surowe:

  • ✅ tylko typy wbudowane (int, double, bool, char, string) oraz enum
  • ✅ zawsze niejawnie static — nie potrzebujesz (i nie możesz) dopisać tego słowa
  • ❌ nie może być typem referencyjnym poza string (nie zrobisz const List<int>)
  • ❌ nie może zależeć od wyniku metody, nawet jeśli metoda zawsze zwraca to samo
// ❌ Błąd kompilacji — DateTime.Now nie jest znane w czasie kompilacji
public const DateTime Deployed = DateTime.Now;

// ✅ OK — wartość jest literałem znanym na etapie kompilacji
public const int MaxRetries = 3;

Gdzie naprawdę żyje wartość const?

To jest sedno tego artykułu. Kompilator nie przechowuje const jako pola w skompilowanym typie. Zamiast tego, wszędzie tam, gdzie w kodzie użyjesz MaxRetries, kompilator podmienia to odwołanie na literał 3 — dosłownie wypala (inlinuje) wartość w miejscu użycia, w kodzie IL.

Konsekwencja: jeśli MaxRetries jest zdefiniowane w jednym assembly (np. Shared.dll), a używane w innym (np. WebApp.dll), to wartość 3 zostaje zapisana fizycznie wewnątrz WebApp.dll podczas jego kompilacji. Zmiana const w Shared.dll i podmiana samego pliku .dll nic nie daWebApp.dll trzeba przebudować, żeby zobaczyć nową wartość.

💡 Tip: to jeden z najczęściej pomijanych tematów na rozmowach rekrutacyjnych o const vs readonly. Sama definicja “stała czasu kompilacji” mało kogo przekonuje – dopiero przykład z wersjonowaniem bibliotek pokazuje, dlaczego to naprawdę ma znaczenie.

Czym jest readonly?

readonly to stała czasu wykonania (runtime constant). Wartość może być przypisana w miejscu deklaracji albo w konstruktorze — i to wystarczy, żeby otworzyć zupełnie inne możliwości niż const.

public class Order
{
    public readonly Guid Id;
    public readonly DateTime CreatedAt;

    public Order()
    {
        Id = Guid.NewGuid();          // ✅ OK — przypisanie w konstruktorze
        CreatedAt = DateTime.Now;     // ✅ OK — wartość nieznana w czasie kompilacji
    }
}

Każda instancja Order dostaje własne Id i CreatedAt – bo readonly (bez static) jest polem instancji, tak jak zwykłe pole, tylko z jednym zastrzeżeniem: po wyjściu z konstruktora nie można go już zmienić.

var order = new Order();
order.Id = Guid.NewGuid(); // ❌ Błąd kompilacji — pole readonly poza konstruktorem

To fundamentalna różnica względem const: readonly nie jest wypalane w IL u konsumenta. Jest odczytywane w runtime, z rzeczywistego pola obiektu – więc zmiana logiki w konstruktorze biblioteki i podmiana samego .dll działa poprawnie, bez rekompilacji Twojego projektu.

Czym jest static readonly?

static readonly łączy obie cechy: podobnie jak readonly – wartość może zależeć od runtime; podobnie jak static – jest jedna dla całego typu, a nie osobna dla każdej instancji.

public class AppConfig
{
    public static readonly HttpClient Client = new HttpClient();
    public static readonly string Environment = System.Environment.GetEnvironmentVariable("ENV") ?? "production";
}

Wszystkie instancje (i cały kod korzystający z AppConfig) współdzielą dokładnie ten sam obiekt Client. Wartość jest ustawiana raz – albo w miejscu deklaracji, albo w konstruktorze statycznym (static AppConfig() { ... }) — i trwa przez cały czas życia aplikacji (a dokładniej: przez czas życia AppDomain).

💡 Tip: to dlatego HttpClient w .NET rekomenduje się trzymać jako static readonly – tworzenie nowej instancji przy każdym requeście prowadzi do wyczerpania socketów (problem znany jako “socket exhaustion”). Współdzielona instancja rozwiązuje to od razu.

Tabela porównawcza

Cechaconstreadonlystatic readonly
Kiedy ustalana wartośćczas kompilacjiczas wykonania (konstruktor)czas wykonania (konstruktor statyczny)
Poziomtyp (niejawnie static)instancja obiektucały typ (współdzielone)
Dozwolone typyproste + string + enumdowolnedowolne
Gdzie fizycznie żyje wartośćwypalona (inlined) w IL u konsumentapole instancji w pamięcijedno pole statyczne, wspólne
Bezpieczna zmiana w bibliotece bez rekompilacji konsumenta❌ nie✅ tak✅ tak
Typowe zastosowaniematematyczne stałe, limity znane na zawszedane ustalane per obiekt (Id, timestamp)współdzielone zasoby (HttpClient, Logger, konfiguracja)
const vs readonly vs static readonly w C#

Pułapka, o której nikt nie mówi

Wróćmy do przykładu z początku artykułu. Problem z const w publicznych bibliotekach (NuGet, wspólne DLL-e w firmie) to nie teoria – to realny incydent, który zdarza się w projektach z wieloma zespołami:

  1. Zespół A publikuje Shared.dll z public const int MaxRetries = 3;
  2. Zespół B buduje WebApp.dll, który korzysta z MaxRetries – kompilator wypala 3 bezpośrednio w kodzie WebApp.dll
  3. Zespół A zmienia wartość na 5 i wydaje nową wersję Shared.dll
  4. Ktoś podmienia tylko plik Shared.dll na serwerze (bez rebuildu WebApp.dll)
  5. Aplikacja dalej używa 3 – bo ta wartość nigdy nie była “czytana” z Shared.dll w runtime, tylko skopiowana do WebApp.dll podczas kompilacji

Żadnego wyjątku, żadnego logu błędu. Po prostu cichy, trudny do zdiagnozowania bug. Dlatego w bibliotekach współdzielonych między projektami/zespołami static readonly jest zwykle bezpieczniejszym wyborem niż const, nawet jeśli wartość “nigdy się nie zmienia”.

Kiedy używać którego – szybka decyzja

  • ✅ Użyj const, gdy wartość jest naprawdę uniwersalna i matematyczna (Math.PI, liczba dni tygodnia) i na pewno zostanie w tym samym assembly co jej konsumenci.
  • ✅ Użyj readonly, gdy każda instancja powinna dostać własną, niemutowalną po utworzeniu wartość (Id, timestamp utworzenia, wstrzyknięta zależność).
  • ✅ Użyj static readonly, gdy potrzebujesz jednej, współdzielonej instancji na cały typ – zwłaszcza dla zasobów, które są drogie w tworzeniu (HttpClient) albo pochodzą z konfiguracji/środowiska.
  • ✅ W bibliotekach publicznych i wspólnych pakietach NuGet – domyślnie wybieraj static readonly zamiast const, chyba że masz absolutną pewność co do wersjonowania.

Wybór między const/readonly a typem wartościowym czy referencyjnym ma znaczenie — zobacz Value types vs reference types w C#.

Najczęstsze błędy

Błąd 1: const dla wartości “na razie stałej” – ktoś deklaruje const int MaxUsers = 100;, zakładając, że to się nigdy nie zmieni. Pół roku później limit trzeba podnieść, a zmiana wymaga rekompilacji i redeploy wszystkich zależnych projektów, zamiast prostej podmiany jednego pliku.

// ❌ Ryzykowne w bibliotece współdzielonej
public const int MaxUsers = 100;

// ✅ Bezpieczniejsze — można zmienić bez rekompilacji konsumentów
public static readonly int MaxUsers = 100;

Błąd 2: mylenie readonly z niemutowalnością obiektureadonly chroni tylko referencję/wartość samego pola, nie zawartość obiektu, na który wskazuje.

public readonly List<string> Names = new List<string>();

Names = new List<string>(); // ❌ Błąd kompilacji
Names.Add("Ala");           // ✅ To działa! Lista wciąż jest mutowalna

Jeśli potrzebujesz przypomnienia podstaw zmiennych i typów w C#, zacznij od Zmienne i typy danych w C#. Alternatywą dla const bywa też enum — zobacz Enum w C#.

Podsumowanie

  • const – wartość znana w czasie kompilacji, wypalana (inlined) u każdego konsumenta; zmiana wymaga rekompilacji wszystkich zależnych projektów.
  • readonly – wartość ustalana w runtime, osobna dla każdej instancji, można ustawić w konstruktorze.
  • static readonly – jak readonly, ale jedna, współdzielona wartość na cały typ.
  • W bibliotekach współdzielonych między zespołami/projektami static readonly jest zwykle bezpieczniejszym domyślnym wyborem niż const.

Krótkie pytania i odpowiedzi

Czy readonly spowalnia działanie kodu? Nie. readonly to zwykłe pole odczytywane tak samo szybko jak każde inne – jedyna różnica to sprawdzenie przez kompilator, że nie próbujesz go zmienić poza konstruktorem.

Czy wartość static readonly da się zmienić przez refleksję? Technicznie tak – refleksja może obejść modyfikator readonly. W praktyce to zły pomysł poza bardzo specyficznymi scenariuszami (np. testy jednostkowe mockujące zależności) i nie powinno być traktowane jako sposób na “legalną” zmianę stałej.

👨‍💻
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ę →