Consider the following executable example:
namespace MyNamespace;
public record struct Record()
{
public bool DoSomething { get; set; } = false;
public void SetDoSomething(bool newValue)
{
DoSomething = newValue;
}
}
public static class Program
{
public static readonly Record MyObject = new();
public static void Main()
{
MyObject.SetDoSomething(true);
Console.WriteLine($"MyObject.DoSomething: {MyObject.DoSomething}");
/* Output:
* false - current version
* true - if MyObject is not readonly or Record is defined as record class
*/
}
}
I'm trying to understand, why DoSomething
is still false, after calling the method which sets the property to true.
My guess is, that a copy gets created when calling the method. It makes sense that this does not happen if Record
is a reference type (record class). But why gets MyObject
not copied if I remove the readonly modifier?
CodePudding user response:
It is called Defensive Copy, which is performed by the C# compilers to enforce the semantic of the value types, it is generally not recommended to mark readonly
on a non-readonly struct since such things will happen and further causes performance regression, there're also some similar scenarios worth mentioning, more specifically:
x.Y
causes a defensive copy of the x if:
- x is a
readonly
field and- the type of x is a non-
readonly
struct and- Y is not a field.
The same rules are applied when x is an in-parameter, ref readonly local variable or a result of a method invocation that returns a value by readonly reference.
you can check more information at The ‘in’-modifier and the readonly structs in C# and Avoiding struct and readonly reference performance pitfalls with ErrorProne.NET
CodePudding user response:
The behaviour you see is present not only in record structs, but also non-record structs too. Try removing the keyword record
and the ()
after the name Record
, and see the same behaviour.
This is just how calling mutating methods on structs are supposed to work. When you call a mutating method on a struct variable, say x.F()
, you actually pass a reference to x
, then that reference can be mutated by F
.
For example, if Record
is a non-record struct, and MyObject
is not readonly
, MyObject.SetDoSomething(true);
is compiled to the following IL (Try it yourself with SharpLab):
ldsflda valuetype Record Program::MyObject
ldc.i4.1
call instance void Record::SetDoSomething(bool)
ldsflda
means "load static field address". I've only found a small section of the spec that talks about this when it is talking about boxing of structs (emphasis mine):
Similarly, boxing never implicitly occurs when accessing a member on a constrained type parameter when the member is implemented within the value type. For example, suppose an interface ICounter contains a method Increment, which can be used to modify a value. If
ICounter
is used as a constraint, the implementation of theIncrement
method is called with a reference to the variable thatIncrement
was called on, never a boxed copy.
Basically, if you don't box structs (you clearly don't here!), their methods are supposed to be called by reference. No copies are supposed to be made.
On the other hand, if you call x.F()
but x
is readonly
, you obviously can't translate it to the same code above, since that would mutate the field. What the compiler does, according to SharpLab, is:
ldsfld valuetype Record Program::MyObject
stloc.0
ldloca.s 0
ldc.i4.1
call instance void Record::SetDoSomething(bool)
Basically, it loads the value of the struct to a temporary variable first, and then pass the reference of that variable to SetDoSomething
.
var temp = MyObject;
temp.SetDoSomething();
Hence the "copy" behaviour that you see.