Search code examples
c#.net-corec#-8.0

In C#, why is readonly only applicable to properties in structs?


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).


Solution

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

    enter image description here

    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