C# readonly Keyword Usage: Fields, Structs, and ref readonly Returns
Learn how the C# readonly keyword works for fields, structs, and ref readonly returns, with practical examples and common mistakes to avoid.
The readonly keyword in C# is often introduced as a simple way to make a field immutable, but its behavior changes depending on where you apply it. It can be used on fields, structs, struct members, and ref readonly returns; the related in parameter modifier provides read-only reference semantics for arguments. Each use has its own constraints and performance implications, so understanding how the compiler enforces each form is more useful than memorizing a single definition. Some of these features depend on the language version: readonly struct, in, and ref readonly require C# 7.2 or later, and readonly members require C# 8.0 or later.
What readonly Means for Fields
An instance readonly field can be assigned only in its declaration or in a constructor of the same class. A static readonly field can be assigned only in its declaration or in a static constructor. Any attempt to assign it elsewhere is a compile-time error. This is useful for values that should not change after an object is built.
public class ServiceOptions { public readonly int TimeoutSeconds; public readonly string Endpoint; public ServiceOptions(int timeoutSeconds, string endpoint) { TimeoutSeconds = timeoutSeconds; Endpoint = endpoint; } }
Unlike const, a readonly field is a runtime value. It can differ from one instance to another, and it cannot be reassigned after construction. This distinction matters because the value can differ per instance, unlike a const field, which is compile-time and shared across all instances.
readonly vs const: Choosing the Right Immutability
const values are inlined at compile time, so they must be literals or expressions composed of other constants. readonly fields are evaluated at runtime, so they can be assigned from method calls, configuration, or computed values. Use const when the value is truly fixed for all builds, such as a version number or a mathematical constant. Use readonly when the value is fixed per instance or per initialization but not known at compile time.
| Aspect | const | readonly |
|---|---|---|
| Assigned at | Compile time | Runtime |
| Scope | Static, shared | Per instance (unless static readonly) |
| Allowed values | Literals, constants | Any expression in constructor |
| Memory | Inlined, no field storage | Stored in object or static storage |
A common mistake is using const for values that might change in future releases. Because const is inlined into every reference, changing the value requires recompiling all dependent assemblies. readonly avoids that because the value is read from the field at runtime.
Readonly Structs and Their Members
Marking a struct as readonly changes how the compiler enforces its members. A readonly struct cannot contain instance fields that are not readonly, and every instance member is treated as non-mutating. This allows the compiler to avoid defensive copies when the struct is passed by in or returned with ref readonly.
public readonly struct Point { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } }
In a readonly struct, instance fields must be readonly, and property setters are not allowed. You can also mark individual members of a non-readonly struct as readonly. This is useful when only one member needs to guarantee that it won't change state.
public struct Rectangle { public double Width; public double Height; public readonly double Area => Width * Height; }
The readonly modifier on Area tells the compiler that the property does not modify the struct's instance state. This can eliminate defensive copies when the struct is passed by in or handled through a ref readonly return.
Ref Readonly Returns and In Parameters
The readonly modifier appears in ref readonly returns, and the related in parameter modifier applies the same read-only guarantee to method arguments. A ref readonly return lets you return a reference to a value without letting the caller modify it through that reference. This is common in high-performance code where copying a large struct would be expensive.
private Point _origin; public ref readonly Point GetOrigin() { return ref _origin; }
The caller can read the returned value but cannot assign to it. It can either copy the value normally or consume it with a ref readonly local:
ref readonly Point origin = ref GetOrigin();
The in parameter is the counterpart for method arguments: it passes a reference but prohibits modification.
public static double Distance(in Point a, in Point b) { double dx = a.X - b.X; double dy = a.Y - b.Y; return Math.Sqrt(dx * dx + dy * dy); }
These features are part of the broader effort to reduce copying in C#, especially when working with large structs in tight loops.
Performance and Runtime Behavior
The readonly modifier on a field is a compile-time constraint; it does not itself change runtime performance. The performance benefits appear with readonly structs and readonly members because the compiler can avoid defensive copies.
When a struct is passed by value, the entire struct is copied. An in parameter passes by reference, but accessing a member that could mutate the struct can force a defensive copy. Marking a member readonly removes that copy because the compiler knows the member will not change the struct's instance state. The actual benefit depends on the size of the struct and how often the member is called.
readonly does not make an object thread-safe. It only prevents the field from being reassigned. If the field is a reference type, the object it points to can still be modified by any code that holds the reference.
Common Mistakes and Edge Cases
One frequent mistake is applying readonly to a field of a mutable reference type and assuming the object cannot change. The readonly keyword only prevents reassigning the field, not calling methods on the object.
public readonly List<int> Items; public void AddItem(int value) { Items.Add(value); // Allowed }
Another edge case is using readonly with arrays. The array reference is readonly, but elements can still be modified. If you need a truly immutable collection, consider ReadOnlyCollection<T> or immutable collections.
Be careful using readonly with structs. A readonly struct can contain a field of a mutable reference type, but the referenced object can still be changed. The compiler enforces that the field reference cannot be reassigned, not that the target object is immutable.
When to Use Readonly: Practical Guidelines
Use readonly for fields that are set once during initialization and represent the identity or configuration of an object. This makes the intent clear and prevents accidental reassignment later.
Consider readonly structs for immutable value types that are passed frequently, especially large ones where copying is expensive. Mark individual members as readonly when they are pure and you use in parameters or ref readonly returns.
Avoid relying on readonly to make a mutable reference type immutable; the keyword gives a false sense of immutability in that case. Also avoid const for values that may change across versions, because const values are inlined.
In summary, readonly has three related roles: preventing field reassignment, marking structs and their members as non-mutating, and supporting ref readonly and in behavior without allowing writes. The right choice depends on whether you need runtime assignment, copy avoidance, or a compile-time constant.