Ref-Readonly and In
05/01/2025
5 minutes
The current version of C# (C# 13 at the time of writing) has in
and ref
keywords. In this post I investigate using these keywords as parameter modifiers. Specifically, the difference between ref readonly
and the in
parameter modifier.
Both parameter modifiers are well-suited for methods that have large struct
parameters. Regular struct
parameters follow copy-by-value semantics. Larger struct copies incur a performance penalty as the larger the struct is the more data has to be copied. I investigated the cost of copies in a post about inline arrays. One approach to address this problem is by passing a reference to the struct
parameters.
In modifier
An author of a method could use the in
parameter modifier for value type parameters. In the following example as RegularStruct
is passed to a method named PassByIn
, with the in
modifier:
var regularValue = new RegularStruct(1); ref readonly var result = ref PassByIn(regularValue); [MethodImpl(MethodImplOptions.NoInlining)] ref readonly int PassByIn(in RegularStruct value) => ref value._field; struct RegularStruct(int value) { public int _field = value; [MethodImpl(MethodImplOptions.NoInlining)] public void Increment() { _field++; } }
The callsite does not need to declare this modifier, and the compiler will pass regularValue as a reference avoiding the copy. If the struct was modified by the PassByIn
method, the compiler would execute it on a copy of the original value. In the following example the value printed on the console shows 1:
var regularValue = new RegularStruct(1); var result = PassByIn(regularValue); Console.WriteLine(result); [MethodImpl(MethodImplOptions.NoInlining)] ref readonly int PassByIn(in RegularStruct value) { value.Increment(); return ref value._field; }
The generated assembly code shows how the defensive copy is made prior to invoking the Increment method.
Program.<<Main>$>g__PassByIn|0_0(RegularStruct ByRef)
L0000: push rbx
L0001: sub rsp, 0x30
L0005: xor eax, eax
L0007: mov [rsp+0x28], rax
L000c: mov rbx, rcx
L000f: mov ecx, [rbx]
L0011: mov [rsp+0x28], ecx
L0015: lea rcx, [rsp+0x28]
L001a: call 0x00007fff59e90048
L001f: mov rax, rbx
L0022: add rsp, 0x30
L0026: pop rbx
L0027: ret
RegularStruct.Increment()
L0000: inc dword ptr [rcx]
L0002: ret
Note that, omitting
[MethodImpl(MethodImplOptions.NoInlining)]
attribute would generate more efficient code, as the JIT can eliminate the copy and the call to the Incremenet method as well.
Ref-Readonly modifier
Replacing the in
parameter modifier with ref readonly
generates the same assembly code with the same behavior. The C# compiler issues a warning for the callsite of the PassByRefRO method indicating that ref
modifier should be used for the ref
parameter, as shown below.
var regularValue = new RegularStruct(1); // When the 👇 ref keyword is omitted a compiler warning is generated: // warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword ref readonly var result = ref PassByRefRO(ref readonlyValue); Console.WriteLine(result); [MethodImpl(MethodImplOptions.NoInlining)] ref readonly int PassByRefRO(ref readonly RegularStruct value) { value.Increment(); return ref value._field; }
Readonly structs
The ref
and in
parameters avoid the copy at the method invocation, but the Increment method call results in a defensive copy. This negates the previous performance gains. To better address this problem, a developer could declare the struct
type as readonly. This way the compiler validates that the type is immutable.
readonly struct ReadOnlyStruct(int value) { public readonly int _field = value; // 👈 must be readonly //public void Increment() //{ //_field++; 👈 not allowed //} }
Adding the previous Increment method as a member to this type results in a compilation error: error CS0191: A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)
This way unintentional copies can be prevented at compile time:
var readonlyValue = new ReadOnlyStruct(1); ref readonly var result = ref PassByRefRO(ref readonlyValue); Console.WriteLine(result); [MethodImpl(MethodImplOptions.NoInlining)] ref readonly int PassByRefRO(ref readonly ReadOnlyStruct value) => return ref value._field; readonly struct ReadOnlyStruct(int value) { public readonly int _field = value; }
Warnings and Errors at callsite
The runtime behavior of in
and ref-readonly
parameters are very close. Yet at compile the C# language differentiates them:
ref readonly parameters issue a warning to use
ref
orin
modifiers at the callsiteref readonly parameters with
ref
modifier issues a compiler error when passing rvalues
For example, the following expression PassByRefRO(ref new ReadOnlyStruct(1));
generates a compiler error: error CS1510: A ref or out value must be an assignable variable
Conclusion
After examining ref readonly
and in
parameter modifiers, we can conclude:
Both modifiers help avoid unnecessary copying of large value types by passing them by reference
The runtime behavior is nearly identical for both approaches
Key differences appear at compile time:
in
parameters don't require modifiers at the callsiteref readonly
parameters enforces ref indication at callsite by generating warnings ifref
orin
modifiers are missingref readonly
withref
modifier at the callsite prevents using rvalues (temporary values) as arguments
Using
readonly struct
provides additional compile-time safety by preventing unintended modifications and avoiding defensive copies
For a complete reference of modifier combinations and their behavior, consult the official documentation.