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.)
The compiler enforces that struct types passed as in arguments and their struct members are read-only variables when used as arguments to other methods.
A struct type is not immutable. Yes, strings are. Making your own type immutable is easy, simply don't provide a default constructor, make all fields private and define no methods or properties that change a field value. Have a method that should mutate the object return a new object instead.
Read only means that we can access the value of a property but we can't assign a value to it. When a property does not have a set accessor then it is a read only property. For example in the person class we have a Gender property that has only a get accessor and doesn't have a set accessor.
If it's private and readonly , the benefit is that you can't inadvertently change it from another part of that class after it is initialized. The readonly modifier ensures the field can only be given a value during its initialization or in its class constructor.
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/
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 }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With