public class MyClass
{
public readonly List<int> Value { get; } = []; // Error: Property cannot be 'readonly'
}
public record MyRecord
{
public readonly string Name { get; init; } // Error: Property cannot be 'readonly'
}
public struct MyStruct
{
public readonly string Name { get; init; } // OK
}
As shown in the code, readonly
cannot be applied to a property, not matter the access modifier is { get; set; }
, { get; }
or { get; init; }
. However, there is no error generated when doing the same thing to a property in a struct (though I understand this is redundant).
There are other posts answering why readonly
isn’t allowed with properties (in classes and records), but I would like to know why this is allowed with structs. Does this have something to do with structs being value types?
EDIT: This question is partly explored in Why does C# 8.0 allow readonly members in a struct but not in a class?, which focuses on the effect of readonly methods of a struct. It doesn’t seem to directly explain the rationale of having readonly properties.
I found that readonly properties are translated to regular properties backed by a readonly field in compiler-generated low-level code. I am not sure if this is the only difference, and the question of why this cannot be applied to classes and records remains (as property + readonly back field can be manually done in classes).
It doesn’t seem to directly explain the rationale of having readonly properties.
Internally properties are just methods:
Properties appear as public data members, but they're implemented as special methods called accessors.
In this case the readonly
is actually redundant as far as I can see, for example Rider marks it as one:
but lets check out the following:
public class C {
public void M(in MyStruct s) {
Console.Write(s.Name);
}
public void M1(in MyStruct s) {
Console.Write(s.Name1);
}
}
public struct MyStruct
{
public readonly string Name => "";
public string Name1 => "";
}
Which results in the following JIT ASM (via sharplab.io):
C.M(MyStruct ByRef)
L0000: mov ecx, [0x6a63fcc]
L0006: call dword ptr [0x1e48f3a8]
L000c: ret
C.M1(MyStruct ByRef)
L0000: cmp [edx], dl
L0002: mov ecx, [0x6a63fcc]
L0008: call dword ptr [0x1e48f3a8]
L000e: ret
Which contains the defensive copy for the non-readonly property.
As for example in the question - no difference for readonly and non-readonly (sharplab.io):
public class C {
public void M2(in MyStruct s) {
Console.Write(s.Name2);
}
public void M3(in MyStruct s) {
Console.Write(s.Name3);
}
}
public struct MyStruct
{
public readonly string Name2 { get; init; }
public string Name3 { get; init; }
}
C.M2(MyStruct ByRef)
L0000: mov ecx, [edx]
L0002: call dword ptr [0x1e48f3a8]
L0008: ret
C.M3(MyStruct ByRef)
L0000: mov ecx, [edx+4]
L0003: call dword ptr [0x1e48f3a8]
L0009: ret