Back to Blog
C#

Understanding `readonly ref` in C#

c# readonly ref: Learn how `readonly ref` in C# enables returning and passing large structs by reference without mutation, reducing copies and improving performance.

C#ref returnsreadonly structsperformancememory management
Illustration of a readonly reference pointing to a struct in memory, with a lock icon indicating read-only access

When a method returns a large struct, the default behavior is to copy the entire value. For types like Vector3 or BigInteger, that copy can dominate the cost of the call. The c# readonly ref feature addresses this by allowing a method to return a reference to a struct while preventing the caller from modifying it. This combines the efficiency of a reference with the safety of a read-only view.

What readonly ref Means in C#

The readonly ref syntax appears in two main forms: ref readonly for return values and in for parameters. Both indicate that the underlying value is passed by reference but cannot be modified through that reference. When you see ref readonly in a method signature, it means the method returns a reference to a value that the caller cannot assign to. Similarly, an in parameter is a read-only reference to an argument.

public ref readonly Vector3 GetPosition() { return ref _position; } public double Distance(in Vector3 a, in Vector3 b) { double dx = a.X - b.X; double dy = a.Y - b.Y; return Math.Sqrt(dx * dx + dy * dy); }

In the first method, GetPosition returns a reference to the internal _position field. The caller can read Position.X but cannot assign Position = new Vector3(). The second method takes in parameters, which means the arguments are passed by reference but the method cannot modify them. This is the foundation of avoiding copies for large structs.

Using ref readonly Returns to Avoid Copies

Returning a large struct by value forces a copy at the call site. If the struct is part of a collection or a long-lived object, that copy happens every time the property is accessed. A ref readonly return eliminates the copy because the caller receives a reference to the original data.

public class Mesh { private BoundingBox _bounds; public ref readonly BoundingBox Bounds => ref _bounds; }

Here, Bounds returns a reference to the internal _bounds field. The caller can read mesh.Bounds.Min and mesh.Bounds.Max without copying the entire BoundingBox. The compiler enforces that the caller cannot assign to mesh.Bounds directly. This pattern is useful for frequently accessed properties that return large structs.

One important detail is that the returned reference must point to a field or an array element, not a local variable. The compiler will reject a ref readonly return that refers to a local because the local would be destroyed when the method exits. This restriction ensures the reference remains valid after the method returns.

Passing Structs by in for Read-Only Parameters

The in parameter modifier is the parameter-side counterpart of ref readonly. It tells the compiler to pass the argument by reference but treat it as read-only inside the method. This is particularly beneficial when the method only reads the struct, as it avoids copying the entire argument.

public static float ComputeArea(in Rectangle rect) { return rect.Width * rect.Height; }

Callers can pass a Rectangle directly: ComputeArea(myRect). The compiler may pass a reference to myRect instead of copying it. However, there are cases where the compiler must make a defensive copy. If the method passes the in parameter to another method that expects a by-value parameter, or if it stores the parameter in a field, the compiler may create a local copy to preserve the read-only guarantee. This is a subtle but important behavior: in does not guarantee zero copies in all scenarios.

readonly ref struct for Stack-Only Types

A readonly ref struct is a struct that is both a ref struct (restricted to the stack) and readonly (all fields are readonly). This combination is used for types that must not be boxed or escape to the heap, such as Span<T> and ReadOnlySpan<T>. The readonly modifier ensures that all instance fields are implicitly readonly, preventing accidental mutation.

public readonly ref struct SensorReading { public readonly double Temperature; public readonly double Humidity; public SensorReading(double temperature, double humidity) { Temperature = temperature; Humidity = humidity; } }

This type can only be used on the stack, which makes it safe to pass by reference without worrying about heap allocation. It is ideal for high-performance code that processes large amounts of data in a tight loop, such as parsing network packets or reading binary files.

Performance and Allocation Considerations

The primary benefit of readonly ref is reducing memory traffic. Copying a large struct involves reading and writing many bytes. By passing a reference, the CPU only handles a pointer. This can significantly reduce the number of memory operations in performance-sensitive code paths.

However, the compiler may still insert defensive copies in certain situations. For example, when an in parameter is used in an async method, the compiler must copy it because the method may outlive the original argument. Similarly, if the method stores the in parameter in a field, a copy is required to maintain the read-only contract. These copies are rare but can negate the performance benefit if they occur in a hot loop.

Another consideration is that ref readonly returns prevent the caller from caching the returned reference. The reference is only valid as long as the underlying object is alive. If the object is a field in a class, the reference remains valid as long as the class instance is alive. But if the reference points to an element in an array that is resized, the reference becomes invalid. The compiler cannot always detect these lifetime issues, so the developer must ensure the reference is not stored beyond the lifetime of the source.

Common Pitfalls and How to Avoid Them

One common mistake is trying to return a ref readonly from a property that computes a value on the fly. For example, a property that returns new Vector3(x, y, z) cannot be a ref readonly because the value is a temporary. The compiler will reject this with an error. The solution is to store the value in a field and return a reference to that field.

Another pitfall is using in parameters with methods that are likely to be inlined. The compiler may choose to pass a copy if the method is small and the argument is a simple expression. This is not a correctness issue but can affect performance. To avoid this, you can explicitly use ref if you need a guaranteed reference, but then you lose the read-only guarantee. In practice, in is a hint that the compiler may ignore.

When using readonly ref struct, be aware that it cannot be used in async methods or as a field in a class. The stack-only restriction is strict. If you need to store the data, you must convert it to a regular struct or use a different design.

When to Use readonly ref vs. Other Approaches

Use ref readonly returns when you have a large struct that is frequently accessed and you want to avoid copying it. This is common in game engines, scientific computing, and data processing pipelines. Use in parameters when a method only reads a large struct and you want to avoid copying the argument. This is effective for math functions, comparison methods, and serialization logic.

For small structs (typically 16 bytes or less), the overhead of a reference may exceed the cost of copying. In such cases, passing by value is simpler and often faster. The decision should be based on the size of the struct and the frequency of the operation. A readonly ref approach is also useful when you need to return a reference to an element in a collection without copying it, such as returning a ref readonly to an item in a List<T>.

If you need to modify the value, you must use ref instead of readonly ref. The readonly modifier is a contract that prevents mutation. If the caller needs to update the value, the method must return a mutable reference. The choice between ref and ref readonly is a design decision that should reflect the intended use of the data.

Finally, consider the lifetime implications. A ref readonly return ties the reference to the source object. If the source is a field in a class, the reference is valid as long as the class instance is alive. If the source is an array element, the reference becomes invalid if the array is resized. Ensure that the caller does not store the reference beyond the lifetime of the source, or you risk undefined behavior. The compiler provides some safety checks, but it cannot verify all scenarios, so careful design is required.

c# readonly ref: Avoid Struct Copies with Ref Returns | RYUSLOG DEV