c# ref readonly: Passing Large Structs Without Copies
Learn how c# ref readonly enables passing large structs by reference without copying, while enforcing immutability. Understand syntax, usage, and pitfalls.
c# ref readonly requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The ref readonly modifier in C# lets you pass a value by reference while preventing the callee from modifying it. It is part of the readonly reference feature introduced in C# 7.2, and it is primarily used to avoid copying large structs while enforcing immutability at the call site. This article explains how ref readonly works, where it fits alongside in, ref, and value parameters, and what pitfalls to avoid when using it.
What Does ref readonly Mean in C#?
In C#, when you pass a struct by value, the entire struct is copied. For large structs, that copy can be expensive, especially in hot paths. Passing by ref avoids the copy but gives the callee full write access. The ref readonly modifier (used in return types and local variables) and the in parameter modifier (which is the parameter equivalent) provide a middle ground: the value is passed by reference, but the callee cannot modify it. This is useful for methods that only need to read a struct and want to avoid the copy overhead.
The syntax for a parameter is in, not ref readonly. For example:
public static double Distance(in Point a, in Point b) { // read-only access to a and b }
The ref readonly syntax is used for return values and local variables. For example:
public ref readonly Point GetOrigin() { // return a reference to a readonly field }
This allows the caller to read the returned struct without copying it, and prevents the caller from modifying the referenced value.
Syntax for ref readonly Parameters and Returns
The in parameter modifier is the parameter-side equivalent of ref readonly. It is applied to the parameter declaration:
public void Process(in LargeStruct data) { // data is readonly within this method }
The ref readonly modifier is used for return types:
public ref readonly LargeStruct GetData() { return ref _data; // _data must be a field or array element }
You can also declare a local variable as ref readonly to hold a reference to a readonly value:
ref readonly var item = ref GetData();
This local variable cannot be reassigned to a different reference, and the value it points to cannot be modified through that variable.
The compiler enforces that you cannot call a non-readonly method on a ref readonly variable, because that method could attempt to modify the instance.
When to Use ref readonly Instead of in or ref
The choice between in, ref, and value parameters depends on the size of the type and the need to modify it.
| Modifier | Copy behavior | Mutability | Typical use |
|---|---|---|---|
| (none) | Copies the value | Callee can modify its local copy | Small structs, simple data |
in | No copy (reference) | Callee cannot modify the original | Large structs, read-only access |
ref | No copy (reference) | Callee can modify the original | Need to modify the original value |
out | No copy (reference) | Callee must assign before returning | Output parameters |
Use in (or ref readonly for returns) when you need to pass a large struct and only read it. This avoids copying and enforces immutability. Use ref when the method must modify the original value. Use value parameters when the struct is small (typically 16 bytes or less) or when you need a local copy for mutation.
A common pattern is to define a readonly struct and pass it with in to avoid defensive copies. The compiler can also optimize calls to readonly struct members when the struct is passed with in.
Performance Impact of ref readonly
The main performance benefit of ref readonly is avoiding the copy of large structs. For a struct containing many fields, copying can involve a significant amount of memory traffic. Passing by reference (via in or ref) passes only a pointer, which is typically 8 bytes on a 64-bit system. This reduces CPU work and memory bandwidth usage.
However, there are costs. Using in can introduce a small overhead if the callee accesses the struct frequently, because the JIT may need to add bounds checks or indirection. In practice, the JIT often optimizes these away. The performance gain is most noticeable when the struct is large (e.g., over 64 bytes) and the method is called frequently in a loop.
Note that ref readonly does not automatically make your code faster. If the struct is small, copying is cheaper than the indirection. Always profile your specific scenario. The feature exists to give you control over copying, not to guarantee performance improvements.
Common Pitfalls with ref readonly
One common mistake is trying to return a ref readonly to a local variable. The compiler will reject this because the local variable goes out of scope. You can only return references to fields, array elements, or other locations that outlive the method.
Another pitfall is assuming that in parameters are always passed by reference. The compiler may copy the value if the argument is not a variable, such as a literal or a property return. For example:
void M(in Point p) { ... } M(new Point(1,2)); // This creates a temporary and passes a reference to it
The temporary is a copy, so you lose the benefit. To avoid this, pass a variable or use the in modifier at the call site: M(in point);.
Also, be careful when mixing ref readonly with ref struct types. ref struct types cannot be boxed and have additional restrictions. You can use ref readonly with them, but you must ensure the lifetime rules are satisfied.
Advanced Usage: ref readonly Returns and ref Structs
A ref readonly return is useful when you want to expose a large struct from a property or method without copying it. For example, a class might store a large readonly struct as a field and expose it via a ref readonly property:
public class DataHolder { private readonly LargeStruct _data; public ref readonly LargeStruct Data => ref _data; }
Callers can then read holder.Data without copying the entire struct. The compiler ensures they cannot modify it.
When working with ref struct types, such as Span<T>, you can also use ref readonly to return a readonly reference to a span element. This is common in high-performance code where you want to avoid copying elements.
Note that ref readonly is a compile-time feature. It does not change the runtime behavior of the value itself; it only changes how references are passed and what operations are allowed.