Constants and readonly
const is fixed when you compile and copied into callers; readonly is fixed once an object has been built.
const int MaxRetries = 3; // vs: private readonly int _pageSize;Some values must not change while the program runs. C# gives you two ways to say so, and they are not interchangeable.
const: fixed when you compile
A const is worked out by the compiler and copied into every place that uses it. Because of that it can only hold something the compiler can compute: a number, a bool, a char, a string, an enum value, or null. It must be given its value on the declaration line. The static modifier is not allowed on a const, because one is already shared by every object of the type. Since C# 10 a const string may be an interpolated string, as long as every value inserted into it is also a constant string.
readonly: fixed once the object exists
A readonly field can be assigned on its declaration line or inside a constructor, the code that runs when an object of a class is created, and nowhere else. So two objects of the same class can hold different readonly values, the value can be computed while the program runs, and the type can be anything.
What trips people up
readonly on a field whose type is a class means the field always points at the same object. It does not freeze that object. A readonly List<string> can still have items added to it; you just cannot replace the list.
For a public value that might ever change, prefer public static readonly over public const.
Example
const int MaxRetries = 3; // fixed when this file is compiled
const string Product = "orders";
// Since C# 10 a const string may be interpolated. The $ inserts the value in
// the braces, and every inserted value must itself be a const string.
const string Topic = $"{Product}-queue";
// readonly is per object, so these two hold different page sizes.
Settings small = new Settings(10);
Settings large = new Settings(1000);
Console.WriteLine(MaxRetries);
Console.WriteLine(Topic);
Console.WriteLine(small.PageSize + " " + large.PageSize);
class Settings
{
// Assigned in the constructor, then fixed for the life of the object.
public readonly int PageSize;
public Settings(int pageSize)
{
PageSize = pageSize;
}
}
// Output:
// 3
// orders-queue
// 10 1000When to use it
- Name a retry limit used in three places so one edit changes all of them
- Expose a library default that callers pick up without recompiling, via static readonly
- Stop a field being replaced after an object has been constructed
- Give a queue name or cache key prefix a single home in the codebase
More examples
readonly does not freeze the object
A readonly field of a class type guarantees the reference, not the contents. This is the single most misread thing about readonly, and it is why a public readonly collection field is a leak: anyone holding your object can empty it. Expose a read-only view instead when that matters.
class Basket
{
// readonly means this field always points at the same list.
public readonly List<string> Items = new List<string>();
public void Add(string item)
{
Items.Add(item); // allowed: the list is not frozen
// Items = new List<string>(); // not allowed outside a constructor
}
}Values const cannot hold
The moment a value needs a method call, an object, or anything beyond numbers, bool, char, string, an enum or null, const is out and static readonly is the answer. Reach for this whenever you want a shared default such as a timeout or a retry delay.
class Policy
{
// Allowed: a whole number the compiler can work out.
public const int MaxAttempts = 5;
// Not allowed: TimeSpan is not a compile-time constant type.
// public const TimeSpan Delay = TimeSpan.FromSeconds(30);
// static readonly works, and the value is computed when the type loads.
public static readonly TimeSpan Delay = TimeSpan.FromSeconds(30);
}The versioning trap across assemblies
Ship Pricing.dll version 2 with MaxItems = 80 and drop it next to the unchanged application: the application still prints 50, because 50 was compiled into it. MaxWeight picks up the new value immediately. This is why public const is reserved for values that are true forever, such as a mathematical constant or a protocol marker.
// Inside a library project, Pricing.dll
public class Limits
{
public const int MaxItems = 50; // copied into every caller
public static readonly int MaxWeight = 20; // read at run time
}
// Inside an application that references Pricing.dll
// Console.WriteLine(Limits.MaxItems); compiles as if you wrote 50
// Console.WriteLine(Limits.MaxWeight); reads the field when it runs
Discussion