Search code examples
c#structmethodsreadonly

What does a readonly method on a struct do?


Visual Studio is suggesting for me to make methods on a struct readonly, what does that mean? I thought only fields could be readonly, not methods.

public struct MyStruct {
    ...
    
    // I have this
    public void MyMethod() { ... }
    // VS wants me to write this
    public readonly void MyMethod() { ... }
}

I could not really find any clear description of methods marked as readonly on MSDN, it's all about readonly parameters or ref readonly for return values:

It clearly does not mean that the return value becomes readonly because I am returning void. It appears to me that the effect is that this is now readonly, but I am also wondering if that means it is now the same as an in parameter like ExtMethod(this in MyStruct self), in which case the struct is passed by reference.

Specifically: Does a readonly struct method guarantee that this is passed by reference, or is it still copied entirely (due to being a value type) like structs usually are when passed to methods?

Please see my answer below, it turns out the premise of my question is wrong, method calls on structs are already by reference.


Solution

  • 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
    }