C# const vs readonly vs static readonly: What to Use When
c# const vs readonly vs static readonly: Learn the differences between const, readonly, and static readonly in C#, including initialization timing, memory behavior, an...
In C#, the choice between const, readonly, and static readonly determines when a value is fixed and how it is accessed. The c# const vs readonly vs static readonly decision affects compile-time behavior, runtime initialization, and maintainability. Each modifier serves a distinct purpose, and using the wrong one can lead to subtle bugs or unnecessary restrictions.
What const Means at Compile Time
A const field is a compile-time constant. The compiler replaces every reference to it with the literal value during compilation. This means the value must be known at compile time and cannot change after the assembly is built.
public class MathConstants { public const double Pi = 3.14159; }
Because const values are embedded directly into the calling code, they are implicitly static. You cannot use static const because const is already static. The value is evaluated when the code is compiled, not when the program runs. This has an important consequence: if you change a const value in a library and rebuild only that library, dependent assemblies that were compiled against the old value will still use the old literal until they are recompiled.
Use const for values that are truly invariant across all versions, such as mathematical constants, configuration keys, or fixed string literals that are part of an API contract.
What readonly Provides at Runtime
A readonly field is initialized either at declaration or in the constructor of the containing class. After the constructor finishes, the field cannot be reassigned. Unlike const, a readonly field is not a compile-time constant; its value is resolved at runtime.
public class Settings { private readonly int _maxRetries; public Settings(int maxRetries) { _maxRetries = maxRetries; } }
readonly fields can be instance fields, meaning each object has its own copy. They can also be static if you declare them as static readonly. The key advantage is that the value can be computed at runtime, for example from configuration, environment variables, or a calculation that depends on the constructor parameters.
A common use case is to store an injected dependency or a value that is determined once per instance. Because the field cannot be changed after construction, it helps maintain object immutability without requiring the entire class to be immutable.
How static readonly Combines Both
A static readonly field is a static field that can only be assigned at declaration or in a static constructor. It behaves like a readonly field but is shared across all instances of the type. The value is evaluated once when the type is first accessed.
public class AppConfig { public static readonly DateTime StartupTime = DateTime.UtcNow; }
Here, StartupTime captures the moment the type is first loaded. It is not a compile-time constant, so the value can be based on runtime data. This is useful for caching expensive computations, storing environment-specific settings, or holding a shared immutable object that should not be recreated per instance.
Unlike const, static readonly fields are not embedded into calling code. They are accessed through the type at runtime, so changing the value in a library does not require recompiling dependent assemblies as long as the field name and type remain the same.
Key Differences in Initialization and Scope
The following table summarizes the core differences:
| Modifier | Initialization Time | Scope | Value Known At | Reassignment |
|---|---|---|---|---|
const | Compile time | Implicitly static | Compile time | Never |
readonly | Declaration or constructor | Instance | Runtime | Only in constructor |
static readonly | Declaration or static constructor | Static | Runtime | Only in static constructor |
A const field is always static, so it cannot be used for instance-specific values. A readonly field is typically an instance field, but you can also create a static readonly field when you need a shared runtime constant. The choice between readonly and static readonly depends on whether the value should be per-object or shared across all objects.
Performance and Memory Considerations
const values are copied into every referencing assembly at compile time. This can slightly improve runtime performance because no field access is needed, but it increases the binary size and creates a maintenance risk if the constant changes. There is no memory allocation for a const field because it is replaced by a literal.
readonly and static readonly fields are stored in memory. An instance readonly field occupies space in each object, while a static readonly field occupies a single slot in the type's static storage. Accessing a readonly field is slightly slower than a const because it requires a field read, but the difference is negligible in most applications.
A more important consideration is initialization timing. A static readonly field is initialized when the type is first used, which can cause a one-time cost. If the initialization is expensive, that cost is paid once per application domain. Instance readonly fields are initialized per object, so the cost is repeated for each instance.
Choosing the Right Modifier for Your Scenario
The decision depends on what the value represents and when it must be fixed.
Use const when the value is a compile-time constant that will never change, such as a mathematical constant or a fixed string that is part of the public API. Be aware that changing a const in a library requires recompiling all consumers.
Use readonly when the value is determined at runtime but should not change after an object is constructed. This is common for dependency injection, configuration values that are passed into a constructor, or calculated values that are cached per instance.
Use static readonly when the value is shared across all instances and is determined at runtime. This is suitable for application-wide settings, cached data that is expensive to compute, or a singleton object that should be created once.
For example, if you are reading a connection string from configuration, a static readonly field is appropriate because the value is the same for every instance and is not known at compile time. If you need a per-instance timeout that is passed in the constructor, use an instance readonly field.
Common Pitfalls and Compatibility Issues
One common mistake is using const for values that might change in future releases. Because the value is embedded in consuming code, changing the const in a library without recompiling consumers leaves them with the old value. This can cause subtle versioning issues.
Another pitfall is assuming that readonly fields are compile-time constants. They are not. You cannot use a readonly field in an attribute argument or as a case label in a switch statement, because those require compile-time constants. const fields are required in those contexts.
static readonly fields can be assigned only in a static constructor or at declaration. If you attempt to assign them elsewhere, the compiler issues an error. This restriction prevents accidental mutation and preserves the immutable intent.
Finally, consider interop and reflection scenarios. const values are not visible as fields in the same way as readonly fields; they are treated as literals. If you rely on reflection to enumerate constants, you may need to handle them differently. readonly and static readonly fields are actual fields and appear in reflection results.
Understanding the initialization and access semantics of each modifier helps you write code that is both correct and maintainable. The choice between const, readonly, and static readonly should be driven by whether the value is known at compile time, whether it is per-instance or shared, and how it will evolve over time.