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) orazenum - ✅ zawsze niejawnie
static— nie potrzebujesz (i nie możesz) dopisać tego słowa - ❌ nie może być typem referencyjnym poza
string(nie zrobiszconst 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 da — WebApp.dll trzeba przebudować, żeby zobaczyć nową wartość.
💡 Tip: to jeden z najczęściej pomijanych tematów na rozmowach rekrutacyjnych o
constvsreadonly. 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
HttpClientw .NET rekomenduje się trzymać jakostatic 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
| Cecha | const | readonly | static readonly |
|---|---|---|---|
| Kiedy ustalana wartość | czas kompilacji | czas wykonania (konstruktor) | czas wykonania (konstruktor statyczny) |
| Poziom | typ (niejawnie static) | instancja obiektu | cały typ (współdzielone) |
| Dozwolone typy | proste + string + enum | dowolne | dowolne |
| Gdzie fizycznie żyje wartość | wypalona (inlined) w IL u konsumenta | pole instancji w pamięci | jedno pole statyczne, wspólne |
| Bezpieczna zmiana w bibliotece bez rekompilacji konsumenta | ❌ nie | ✅ tak | ✅ tak |
| Typowe zastosowanie | matematyczne stałe, limity znane na zawsze | dane ustalane per obiekt (Id, timestamp) | współdzielone zasoby (HttpClient, Logger, konfiguracja) |
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:
- Zespół A publikuje
Shared.dllzpublic const int MaxRetries = 3; - Zespół B buduje
WebApp.dll, który korzysta zMaxRetries– kompilator wypala3bezpośrednio w kodzieWebApp.dll - Zespół A zmienia wartość na
5i wydaje nową wersjęShared.dll - Ktoś podmienia tylko plik
Shared.dllna serwerze (bez rebuilduWebApp.dll) - Aplikacja dalej używa
3– bo ta wartość nigdy nie była “czytana” zShared.dllw runtime, tylko skopiowana doWebApp.dllpodczas 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 readonlyzamiastconst, 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ą obiektu – readonly 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– jakreadonly, ale jedna, współdzielona wartość na cały typ.- W bibliotekach współdzielonych między zespołami/projektami
static readonlyjest 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.
🚀 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ę →