Ref-Readonly and In05/01/2025 | 5 minutes to read
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.<$>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) => 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
reforinmodifiers at the callsite - ref readonly parameters with
refmodifier 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:
inparameters don't require modifiers at the callsiteref readonlyparameters enforces ref indication at callsite by generating warnings ifreforinmodifiers are missingref readonlywithrefmodifier at the callsite prevents using rvalues (temporary values) as arguments
- Using
readonly structprovides 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.