Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 253
Erik Bongers Avatar asked Jan 29 '23 01:01

Erik Bongers


1 Answers

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.

like image 191
Evk Avatar answered Jan 30 '23 13:01

Evk