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