Why is Point.Offset() not giving a compiler error in a readonly struct?

345 Views Asked by At

Perhaps I'm misunderstanding the concept of a readonly struct, but I would think this code should not compile:

public readonly struct TwoPoints
{
    private readonly Point one;
    private readonly Point two;

    void Foo()
    {
        // compiler error:  Error CS1648  Members of readonly field 'TwoPoints.one'
        // cannot be modified (except in a constructor or a variable initializer)
        one.X = 5;

        //no compiler error! (and one is not changed)
        one.Offset(5, 5);
    }
 }

(I'm using C# 7.3.) Am I missing something?

2

There are 2 best solutions below

5
On BEST ANSWER

There is no way for compiler to figure out that Offset method mutates Point struct members. However, readonly struct field is handled differently compared to non-readonly one. Consider this (not readonly) struct:

public struct TwoPoints {
    private readonly Point one;
    private Point two;

    public void Foo() {
        one.Offset(5, 5); 
        Console.WriteLine(one.X); // 0
        two.Offset(5, 5);
        Console.WriteLine(two.X); // 5
    }
}

one field is readonly but two is not. Now, when you call a method on readonly struct field - a copy of struct is passed to that method as this. And if method mutates struct members - than this copy members are mutated. For this reason you don't observe any changes to one after method is called - it has not been changed but copy was.

two field is not readonly, and struct itself (not copy) is passed to Offset method, and so if method mutates members - you can observe them changed after method call.

So, this is allowed because it cannot break immutability contract of your readonly struct. All fields of readonly struct should be readonly, and methods called on readonly struct field cannot mutate it, they can only mutate a copy.

If you are interested in "official source" for this - specification (7.6.4 Member access) says that:

If T is a struct-type and I identifies an instance field of that struct-type:

• If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.

• Otherwise, the result is a variable, namely the field I in the struct instance given by E.

T here is target type, and I is member being accessed.

First part says that if we access instance readonly field of a struct, outside of constructor, the result is value. In second case, where instance field is not readonly - result is variable.

Then section "7.5.5 Function member invocation" says (E here is instance expression, so for example one and two above):

• If M is an instance function member declared in a value-type:

If E is not classified as a variable, then a temporary local variable of E’s type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E is a true variable is it possible for the caller to observe the changes that M makes to this.

As we saw above, readonly struct field access results in a value, not a variable and so, E is not classified as variable.

2
On

Supporting the answer from Evk, there is an article in regards the Point's method: Point.Offset()

Note that calling the Offset method will only have an effect if you can change the X and Y properties directly. Because Point is a value type, if you reference a Point object by using a property or indexer, you get a copy of the object, not a reference to the object. If you attempt to change X or Y on a property or indexer reference, a compiler error occurs. Similarly, calling Offset on the property or indexer will not change the underlying object. If you want to change the value of a Point that is referenced as a property or indexer, create a new Point, modify its fields, and then assign the Point back to the property or indexer.


By the definition Point is declared as struct: Point

Last but not least, there is an article on Microsoft's blog, which describes that readonly fields, that are actually struct, their public properties are read-only also.

A read-only struct is a struct whose public members are read-only, as well as the “this” parameter.