Does using public readonly fields for immutable structs work?

16,667

Solution 1

If you're going to use structs, it is a best practice to make them immutable.

Making all the fields readonly is a great way to help (1) document that the struct is immutable, and (2) prevent accidental mutations.

However, there is one wrinkle, which actually in a strange coincidence I was planning on blogging about next week. That is: readonly on a struct field is a lie. One expects that a readonly field cannot change, but of course it can. "readonly" on a struct field is the declaration writing cheques with no money in its account. A struct doesn't own its storage, and it is that storage which can mutate.

For example, let's take your struct:

public struct Pair
{
    public readonly int x;
    public readonly int y;
    public Pair(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public void M(ref Pair p)
    {
        int oldX = x;
        int oldY = y;
        // Something happens here
        Debug.Assert(x == oldX);
        Debug.Assert(y == oldY);
    }
}

Is there anything that can happen at "something happens here" that causes the debug assertions to be violated? Sure.

    public void M(ref Pair p)
    {
        int oldX = this.x;
        int oldY = this.y;
        p = new Pair(0, 0);
        Debug.Assert(this.x == oldX);
        Debug.Assert(this.y == oldY);
    }
...
    Pair myPair = new Pair(10, 20);
    myPair.M(ref myPair);

And now what happens? The assertion is violated! "this" and "p" refer to the same storage location. The storage location is mutated, and so the contents of "this" are mutated because they are the same thing. The struct is not able to enforce the read-only-ness of x and y because the struct doesn't own the storage; the storage is a local variable that is free to mutate as much as it wants.

You cannot rely on the invariant that a readonly field in a struct is never observed to change; the only thing you can rely on is that you can't write code that directly changes it. But with a little sneaky work like this you can indirectly change it all you want.

See also Joe Duffy's excellent blog article on this issue:

http://joeduffyblog.com/2010/07/01/when-is-a-readonly-field-not-readonly/

Solution 2

As of C# 7.2, you can now declare an entire struct as immutable:

public readonly struct Pair
{
    public int x;
    public int y;

    // Constructor and stuff
}

This will have the same effect as marking all of the fields as readonly, and will also document to the compiler itself that the struct is immutable. This will increase the performance of areas where the struct is used by reducing the number of defensive copies the compiler makes.

As noted in Eric Lippert's answer, this does not prevent the structure itself from being reassigned completely, and thus providing the effect of its fields changing out from under you. Either passing by value or using the new in parameter modifier can be used to help prevent this:

public void DoSomething(in Pair p) {
    p.x = 0; // illegal
    p = new Pair(0, 0); // also illegal
}

Solution 3

That would make it immutable indeed. I suppose you better add a constructor though.
If all its members are immutable too, this would make it entirely immutable. These can be classes or simple values.

Solution 4

The compiler will forbid assignment to readonly fields as well as read-only properties.

I recommend using read-only properties mostly for public interface reasons and data-binding (which won't work on fields). If it were my project I would require that if the struct/class is public. If it's going to be internal to an assembly or private to a class, I could overlook it at first and refactor them to read-only properties later.

Share:
16,667
Mike
Author by

Mike

Updated on June 06, 2022

Comments

  • Mike
    Mike almost 2 years

    Is this a proper way to declare immutable structs?

    public struct Pair
    {
        public readonly int x;
        public readonly int y;
    
        // Constructor and stuff
    }
    

    I can't think of why this would run into problems, but I just wanted to ask to make sure.

    In this example, I used ints. What if I used a class instead, but that class is also immutable, like so? That should work fine too, right?

    public struct Pair
    {
        public readonly (immutableClass) x;
        public readonly (immutableClass) y;
    
        // Constructor and stuff
    }
    

    (Aside: I understand that using Properties is more generalizable and allows changing, but this struct is intended literally to just store two values. I'm just interested in the immutability question here.)