2

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.

3
  • 3
    learn.microsoft.com/en-us/dotnet/csharp/language-reference/… Commented Oct 30, 2024 at 15:46
  • 1
    @Serg Right, but that doesn't clearly state whether it means this gets 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 explicit in parameters. It would be good if a readonly method makes that unnecessary, then I wouldn't need to use extension methods. Commented Oct 30, 2024 at 15:52
  • Right now, you're method is a "black box". The readonly keyword (IMO) is a "contract" that says "this method does not modify the contents of the structure" (and the compiler insures it doesn't). And if it doesn't return anything, and it doesn't modify its contents, then what exactly does it do? Commented Oct 30, 2024 at 17:15

3 Answers 3

5

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
}
Sign up to request clarification or add additional context in comments.

Comments

4

Consider the following cases:

public struct MyStruct
{
    public void MyMethod1() {  }
    public readonly void MyMethod2() {  }
}

public static class Test
{
    private static MyStruct field1;
    private static readonly MyStruct field2;
    public static void ExtMethod1(this in MyStruct self) => self.MyMethod1();
    public static void ExtMethod2(this in MyStruct self) => self.MyMethod2();
    public static void ExtMethod3(this MyStruct self) => self.MyMethod2();
    public static void ExtMethod4(this ref MyStruct self) => self.MyMethod1();
    public static void ExtMethod5() => field1.MyMethod1();
    public static void ExtMethod6() => field2.MyMethod1();
    public static void ExtMethod7() => field2.MyMethod2();
}
  1. In ExtMethod1 it is declared that self should not be change. But the compiler sees that the struct is mutable, and I'm calling a method on it, so it has to create a defensive copy. This is probably counter to expecations, so at least my IDE give a warning about it.
  2. In ExtMethod2 only a readonly method is called, so no defensive copy is needed, since the compiler can be sure it has not been modified.
  3. In ExtMethod3 the parameter is not marked as in or ref, so a copy will be made.
  4. In ExtMethod4 it is explicitly declared that self may be mutated, so a copy will not be made.
  5. In ExtMethod5 the field is mutable, so a copy cannot be made, since mutation is intended.
  6. In ExtMethod6 a copy has to be made to avoid mutation. Same as ExtMethod1.
  7. In ExtMethod7 both the field and method are readonly, so no copy is needed. Same as ExtMethod2.

Note that the compiler is free to do whatever optimization it can, as long as it can prove that the behavior does not change. But it is very difficult to prove what a method could do, so the compiler sometimes needs annotations by the developer to figure out what it can and cannot optimize. So while the copy in ExtMethod3 could be removed, there will be no guarantees, and you would need to inspect the assembler to figure out if it is actually done.

3 Comments

A trivial example of the impact of this: imagine a readonly DateTime when field; before readonly, even things like var end = when.Add(offset); would create a defensive copy, for correctness. I have old code with comments like // note: not marked readonly for performance (defensive copy)
I am asking about how this is passed into the method, not about whether it makes defensive copies first. Is it copied into the method as a value or passed as a reference? 'in' guarantees that the marked parameter is passed in by reference. Does a readonly method guarantee the same for the invisible this parameter or does it not?
@Sander If this where copied it would make methods that mutate the struct impossible, since the caller would never see the mutated copy. So no, just calling a member method will not create a copy, unless you call a mutable method on a readonly struct.
0

It simply means that the method cannot change the contents (fields, properties) of the struct.

1 Comment

There's more to it than that, though. JonasH's answer covers it in more depth.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.