Types ยท immutability
const vs readonly vs static readonly
All three say "this won't change" โ but they make that promise at different moments, to different scopes, with different consequences. Getting the wrong one is how configuration values mysteriously become 0.
The one-line version
| Keyword | Decided when | Scope | Who can set it |
|---|---|---|---|
const | compile time | implicitly static | the compiler literally pastes the value in |
readonly | object construction | per object | the constructor (or field initializer) |
static readonly | type initialization | one per class | the static constructor (or initializer) |
const: a compile-time literal
A const must be a literal that the compiler can evaluate before the program ever runs. It is copied into every use site โ which is why it must be a primitive or string:
const int MaxRetries = 3; // compiler knows this number at build time
Console.WriteLine("Max retries: " + MaxRetries);
// const string Version = "1.0"; // fine โ literal
// const double Pi = 3.14159; // fine โ literal
What you cannot do: initialize a const from anything computed at runtime.
// All of these are compile errors:
const int FromMethod = int.Parse("5"); // a method call
const string FromNow = DateTime.Now.ToString(); // runtime value
const List<int> Numbers = new List<int>(); // reference types can't be const
That last one is the important limitation: const only works for value types and strings. For everything else โ a list, a class instance, a computed value โ you need readonly.
readonly: set once per object
A readonly field is assigned in the constructor and never again. Each object gets its own copy:
class Ticket
{
public readonly int Number; // set once, in the constructor
public Ticket(int number)
{
Number = number; // โ allowed: we're in the constructor
}
public void TryChange()
{
// Number = 99; // โ CS0191: readonly field
}
}
var a = new Ticket(1);
var b = new Ticket(2);
Console.WriteLine("Ada: created " + a.Number);
Console.WriteLine("Bob: created " + b.Number);
This is the right tool when the value is fixed for that instance but unknown until construction โ an ID, a creation timestamp, an immutable configuration object's field.
static readonly: set once for the whole class
Same "set once" rule, but shared by every instance. It's set in the static constructor (or a static field initializer) and is the standard home for runtime-computed constants:
class AppConfig
{
public static readonly int StartupYear = 2026; // one copy, shared by every instance
}
Console.WriteLine("Startup year: " + AppConfig.StartupYear);
var c1 = new AppConfig();
var c2 = new AppConfig();
// static members are reached through the CLASS, never through an instance:
Console.WriteLine("Still: " + AppConfig.StartupYear);
Why this matters: static readonly lets you compute the value at startup โ read a config file, call a method, use DateTime.Now โ which const cannot do. This is the idiomatic replacement for "I want a constant but it's a computed value".
The subtle trap: const across assemblies
Because const is baked into call sites at compile time, changing it in one assembly does not update the code that already compiled against it. Ship a library with public const int Version = 1;, bump it to 2, and apps compiled against the old library still see 1 until they recompile.
static readonly has no such problem โ the value is read at runtime, so a new library version takes effect immediately. Rule: use const for genuinely fixed literals (ฯ, day names), static readonly for anything that might change between releases.
Decision table
| What you have | Use |
|---|---|
| A literal that will never change (3, "v1", 3.14159) | const |
| A value computed once at startup (config, environment) | static readonly |
| A value fixed per object but unknown until construction | readonly |
| A list or class instance that shouldn't be reassigned | readonly / static readonly (note: the contents can still change!) |
One trap worth knowing: readonly List<int> Items = new List<int>(); prevents reassigning Items, but Items.Add(5) still works. readonly freezes the reference, not the contents.
Frequently asked
Can a const be assigned from a method call?
No. const must be a compile-time literal (numbers, strings, chars, bools). For anything computed at runtime โ including DateTime.Now, file reads, or new List() โ use static readonly.
Does readonly make a field deeply immutable?
No. It prevents reassigning the field itself. If the field holds a list or an object, that object's contents can still be mutated. For real immutability you need immutable types (records, readonly structs).