TLDR; readonly on a struct method means that this is passed as a readonly reference to the method, rather than a normal reference like with non-readonly struct methods. It also ensures no defensive copies will need to be made when this method is called on readonly variables.
Correction on my part
It seems I was mistaken in the first place to think that structs get passed by value to their own methods.
I've inspected the compiler output of some code examples
with ILSpy (in .NET 8.0, Debug - so they were not optimized) and it seems that in all cases of a method invocation on the struct, the struct is passed to the method by reference and not by value.
public static void CallOnValue(MyStruct mystruct) // passed by value
{
mystruct.NormalMethod(); // passed by reference
}
/* Generated CIL:
ldarga.s mystruct
call instance void ProjName.MyStruct::NormalMethod()
*/
// ldarga = load arg address
// So, the address (= reference) of mystruct is passed to the method
Answer
So yes, readonly methods pass this by reference, but so do other methods.
In this way, a regular struct method behaves effectively(*) the same as a ref extension method, and a readonly struct method behaves effectively(*) the same as an in extension method.
public struct MyStruct
{
public void NormalMethod() { }
public readonly void ReadonlyMethod() { }
}
public static class Test
{
public static void ExtSameAsNormal(this ref MyStruct self) { }
public static void ExtSameAsReadonly(this in MyStruct self) { }
}
* Do note that instance methods and static methods emit slightly different IL:
call instance void ProjName.MyStruct::NormalMethod()
call void ProjName.Test::ExtSameAsNormal(valuetype ProjName.MyStruct&)
but in all 4 example cases, the struct is passed to the method by reference. For extension methods you just have to specify 'in' or 'ref' explicitly to get reference semantics, whilst instance methods do it implicicly.
About defensive copies
On normal methods, if they are called on a readonly reference, the compiler cannot ensure that the method will not modify the struct, so it makes a "defensive copy" and passes a reference of that to the method so that the original struct is guaranteed not to be modified. It does effectively this:
public static void Foo(in MyStruct readonlyStruct) // 'in' = passed as readonly reference
{
readonlyStruct.NormalMethod();
}
// Generates this code when compiled:
public static void Foo(in MyStruct readonlyStruct)
{
MyStruct defCopy = readonlyStruct;
defCopy.NormalMethod(); // defCopy may be modified, but readonlyStruct won't
}
By labeling your method readonly, the method states it won't modify the struct, so readonlyStructVar.ReadonlyMethod(); does not need to make a defensive copy.
One interesting aside, if you really want to avoid defensive copies, you may actually prefer extension methods with a ref parameter over normal methods because they will not generate defensive copies but error instead:
public static void CallOnIn(in MyStruct s)
{
s.NormalMethod(); // will make defensive copy without your knowledge
s.ExtSameAsNormal(); // Error: CS8329 Cannot use variable 's' as a ref or out value because it is a readonly variable
s.ReadonlyMethod(); // no copy needed
s.ExtSameAsReadonly(); // no copy needed
}
thisgets passed by reference or by value. For large structs I do not want them passed by value, so I have been making extension methods with explicitinparameters. It would be good if a readonly method makes that unnecessary, then I wouldn't need to use extension methods.