60

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

Mike
  • 1,037
  • 1
  • 8
  • 13
  • 1
    `readonly` properties/members can only be set from within the constructor (at the latest). They can't be set with the property initialization-syntax. –  May 19 '11 at 18:32
  • You may want to check [Immutable types: understand their benefits and use them](http://codebetter.com/patricksmacchia/2008/01/13/immutable-types-understand-them-and-use-them/) – YetAnotherUser May 19 '11 at 18:34
  • `readonly` only affects the assignment operator. It does not have as strong semantics as C++'s `const` keyword. – Etienne de Martel May 19 '11 at 18:47

4 Answers4

121

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/

paercebal
  • 81,378
  • 38
  • 130
  • 159
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 19
    It's amazing how many edge-cases you demonstrate that make me think, "Who would even *do* that?" – Joel B Fant May 19 '11 at 19:15
  • 4
    I discovered that you can do the same sort of thing with `StructLayout.Explicit` when I was debugging somebody else's code. – Jim Mischel May 19 '11 at 19:51
  • 1
    @Jim: Correct; you can overlay a readonly field and a read-write field into the same storage no problem. It's weird, but it is legal. – Eric Lippert May 19 '11 at 20:12
  • 10
    @Joel: Suppose we come up with a way to multiply pairs, but it sometimes fails. So you have "bool MultiplyBy(Pair x, out Pair result)" as a method which multiplies "this" by x, returns the success or failure as a bool, and writes the result into the aliased variable. Now you have a pair and you want to square it and replace the previous value with the square, so you say "myPair.MultiplyBy(myPair, out myPair)". And boom, you've put yourself into this awful trap. Each step along the way was pretty reasonable, but they add up to something horrid. – Eric Lippert May 19 '11 at 20:15
  • Let me see if I understand what happened: You passed it in with the ref modifier, which meant that rather than passing in a copy of the value of the variable, you passed in the variable itself. But since the struct is contained within the variable, as opposed to the variable containing a reference to the object (as for classes), overwriting this variable effectively overwrote the struct. I don't understand how this would turn out differently with properties though instead of readonly fields. – Mike May 19 '11 at 20:50
  • @Mike: Your understanding is correct. It doesn't turn out differently with properties; you can mutate a property that only has a getter in exactly the same way. A read-only property in a struct also provides no guarantee that it always gives the same value inside the same method. – Eric Lippert May 19 '11 at 20:58
  • I see. So this applies generally then. Is there basically no airtight way to ensure immutability then? – Mike May 19 '11 at 21:04
  • 2
    @Mike: *variables can change*. That's why they're called "variables". If what you want is to ensure that a particular *variable* never changes, make that readonly. Just remember that a struct is always borrowing a variable from someone else; if you want the fields of a struct to never change, make them readonly and ensure that the variable which the struct is borrowing storage from is also unchanging. Also, of course the problem I am sketching out here does not arise frequently in practice. – Eric Lippert May 19 '11 at 21:50
  • This answer is very confusing and probably one of the reasons people don't get the difference between variables and values. A variable contains one value at a given time, and while the *contained value* can be exchanged for another through reassignment, a value itself can never change. The integer 2 will always be the integer 2, and a Pair(2, 2) will always be a Pair(2, 2). Your code doesn't mutate the value in myPair (since Pair is immutable), it merely reassigns the variable - putting an entirely new Pair value in it. So of course your assertion must fail. – Askaga Dec 18 '14 at 15:53
  • 3
    @BillAskaga: I agree that people are frequently confused by the difference between values and variables, but I don't see how the blame for that can be laid upon this answer; the problem seems to be more general and widespread. I think you are missing the point of my answer. I'm not suggesting that the *value* is mutated, as you correctly note, values are just values; I'm pointing out that *the contents of a readonly field may be observed to change over time even outside of a constructor*, which is counterintuitive. – Eric Lippert Dec 18 '14 at 16:20
  • 3
    @EricLippert: I just think you should explain more clearly that the *x/y* fields really do not change, but the underlying *this* reference does which causes the *x/y* fields to *seemingly* change. So the major problem is C# not allowing us to make the *myPair* field readonly - thus preventing us from reassigning it in the first place. The fact that the same code behaves differently when Pair is a class is of course quite counterintuitive. – Askaga Dec 23 '14 at 18:14
  • 1
    In C#7.2 we now can use `readonly struct` for real. https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-2 – 5argon Apr 18 '18 at 11:37
  • @5argon: That doesn't help as much as you'd think. Eric's example still compiles. See also https://stackoverflow.com/a/51602000/18192 – Brian Aug 13 '18 at 19:13
7

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
}
TheHans255
  • 2,059
  • 1
  • 19
  • 36
  • Making a struct `readonly` does not implicitly mark all fields as `readonly`. You must still do that yourself, but gets the compiler to help you find any cases where you forgot. Otherwise you get the error CS8340 'Instance fields of readonly structs must be readonly.' – rbwhitaker Feb 17 '22 at 04:25
  • "This will have the same effect as marking all of the fields as readonly". No, this code sample will not compile until you make both x and y readonly. – Lucas Apr 26 '23 at 13:55
5

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.

Mr47
  • 2,655
  • 1
  • 19
  • 25
1

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.

Joel B Fant
  • 24,406
  • 4
  • 66
  • 67
  • 2
    The compiler is capable of detecting assignments to `readonly` fields at compile-time and will raise a compiler error if one is detected. – dtb May 19 '11 at 18:38
  • The runtime will enforce the `readonly` semantics, too. That is, if `x` is a `readonly` field of type `MyStruct`, and `s` is an instance of `MyStruct`, then `dynamic d = s; d.x = 42;` will throw an exception. – Jim Mischel May 19 '11 at 20:36
  • What are the "public interface reasons"? – TheQuickBrownFox Feb 12 '16 at 15:34