Ref Readonly Parameters: Passing by Reference Without the Pitfalls
C# has long offered in parameters for passing values by readonly reference, avoiding copies of large structs. But in has a subtle problem: callers do not need to annotate anything at the call site, which means a method can silently start taking a reference to a local variable without the caller realising it. C# 12 introduces ref readonly parameters to address this.
The Problem with in
The in modifier was designed to be frictionless. You can call an in parameter method without any special syntax:
void Process(in Matrix4x4 transform) { ... }
Matrix4x4 matrix = Matrix4x4.Identity;
Process(matrix); // No 'in' keyword needed at the call site
This convenience becomes a problem in two scenarios. First, when the caller does not realise a reference is being taken (important for understanding lifetimes). Second, when a library author wants to migrate from ref to a readonly reference — they cannot use in because it would silently change the call site semantics.
There is also a hidden performance trap: if you pass an rvalue (a temporary) to an in parameter, the compiler creates a hidden local copy. The caller has no indication this is happening.
Enter ref readonly
ref readonly parameters require the caller to explicitly use ref or in at the call site:
void Process(ref readonly Matrix4x4 transform) { ... }
Matrix4x4 matrix = Matrix4x4.Identity;
Process(ref matrix); // Caller must be explicit
Process(in matrix); // Also valid
Inside the method, the parameter behaves exactly like in — you cannot modify it:
void Process(ref readonly Matrix4x4 transform)
{
transform = Matrix4x4.Identity; // Error: cannot assign to 'in' parameter
var det = transform.GetDeterminant(); // Fine — reading is allowed
}
When to Use Each
The choice between in, ref, and ref readonly comes down to intent and safety:
// ref: caller and method can both read and write
void Update(ref Vector3 position) { position += velocity; }
// in: pass by readonly reference, no call-site annotation needed
void Render(in Matrix4x4 viewMatrix) { ... }
// ref readonly: pass by readonly reference, call-site annotation required
void Calculate(ref readonly BigStruct data) { ... }
Use ref readonly when:
- You want the performance benefit of pass-by-reference for large value types
- You want the caller to be aware that a reference is being taken
- You are migrating an API from
refto readonly and need to preserve call-site annotation
Use in when:
- Caller convenience matters more than explicitness
- The method is widely used and adding
refeverywhere would be disruptive
API Migration Path
One of the primary motivations for ref readonly is enabling library authors to tighten APIs. If you have a method that takes ref but never actually mutates the parameter, changing it to in would be a source-breaking change because call sites use ref:
// Original
void Analyse(ref LargeData data) { /* only reads data */ }
// Call site
Analyse(ref myData);
// Changing to 'in' breaks the call site — 'ref' is no longer valid
void Analyse(in LargeData data) { ... }
Analyse(ref myData); // Error!
// Changing to 'ref readonly' preserves the call site
void Analyse(ref readonly LargeData data) { ... }
Analyse(ref myData); // Still works, with a warning suggesting 'in'
The ref readonly parameter accepts both ref and in at the call site, making migration non-breaking.
Performance Characteristics
Like in, ref readonly avoids copying the value when passing it. For large structs — anything above roughly 16 bytes — this can make a measurable difference in hot loops:
public readonly struct SensorReading
{
public double Temperature { get; init; }
public double Humidity { get; init; }
public double Pressure { get; init; }
public DateTime Timestamp { get; init; }
public Guid SensorId { get; init; }
// ~56 bytes — definitely worth passing by reference
}
double ComputeAverage(ref readonly SensorReading a, ref readonly SensorReading b)
{
return (a.Temperature + b.Temperature) / 2.0;
}
For small structs (8 bytes or fewer), passing by value is typically faster than passing by reference because the indirection cost outweighs the copy cost.
Practical Guidance
For most application code, in remains the simpler choice. Reach for ref readonly when you are writing library APIs where call-site clarity matters, or when you need a non-breaking migration path from ref to a readonly reference. The feature fills a real gap in C#'s parameter passing story, even if it is not one most developers will use daily.