Search code examples
c#refc#-7.2

C# Behavior of ref readonly


I was reading documentation on C# 7.2 here and I came across this in regards to ref readonly:

The compiler enforces that the caller can't modify the reference. Attempts to assign the value directly generate a compile-time error. However, the compiler can't know if any member method modifies the state of the struct. To ensure that the object isn't modified, the compiler creates a copy and calls member references using that copy. Any modifications are to that defensive copy.

This generated some confusion for me (and possibly some others), so I'd like to clarify the behavior now. Suppose I have a struct defined like this:

public struct Point3D
{
    private static Point3D origin = new Point3D(0,0,0);

    public static ref readonly Point3D Origin => ref origin;

    public int X { get; set; }
    public int Y { get; set; }
    public int Z { get; set; }

    public static void ChangeOrigin(int x = 0, int y = 0, int z = 0)
    {
        origin = new Point3D(x, y, z);
    }
}

Now, suppose I used ref readonly to get Point3D.Origin and modified it:

ref readonly var origin = ref Point3D.Origin;
var originValue = Point3D.Origin;
Point3D.ChangeOrigin(1, 1, 1);

Console.WriteLine("Origin is: ({0}, {1}, {2})", origin.X, origin.Y, origin.Z);
Console.WriteLine("Origin is: ({0}, {1}, {2})", originValue.X, originValue.Y, originValue.Z);

The result from running this code is:

Origin is: (1, 1, 1)
Origin is: (0, 0, 0)

This is expected. The value in origin is updated when I call ChangeOrigin whereas the value in originValue is copied and therefore doesn't change. My question is in regards to the "defensive copy" mentioned above. Why is this necessary? The value in origin can't be changed without invoking compiler errors and the reference is updated properly when Point3D.Origin updates so what reason is there to have an extra copy of the object, which from what I gathered by reading the documentation, isn't updated?


Solution

  • You can assign a value to a readonly field only in the following contexts:

    • When the variable is initialized in the declaration.

      C# example:

       public readonly int y = 5;
      
    • In an instance constructor of the class that contains the instance field declaration.

    • In the static constructor of the class that contains the static field declaration.

    These constructor contexts are also the only contexts in which it is valid to pass a readonly field as an out or ref parameter.

    If you use a statement like the following example:

    p2.y = 66; // Error
    

    you will get the compiler error message:

    A readonly field cannot be assigned to (except in a constructor or a variable initializer)

    The readonly modifier on a ref return indicates that the returned reference cannot be modified. The following example returns a reference to the origin. It uses the readonly modifier to indicate that callers cannot modify the origin:

    private static readonly Point origin = new Point(0, 0);
    public static ref readonly Point Origin => ref origin;
    

    The type returned doesn't need to be a readonly struct. Any type that can be returned by ref can be returned by ref readonly.

    This is all straight from the DOCs, and I hope it clarifies it for you! Happy coding!

    So if you set (1, 1, 1) in the constructer, it will be (1, 1, 1) calling a change with a different method, as above, you get (0, 0, 0)