In Defense of the Mutable Struct

In the world of .NET, there’s a school of thought that tells us that all structs (i.e. value types) should be immutable. I’ve seen it spouted all over the place. I’m not opposed to the idea of immutable types, but I really don’t agree with the notion that all structs should be immutable.

The idea of immutable structs certainly has its case. Bad things happen when what you expect doesn’t correspond with what actually happens…

// Let's move our Form 100 pixels to the right.
this.Location.Offset(100, 0);

It looks like it should work. Luckily, most any C# programmer can tell you that this is incorrect. The problem, in short, is that structs don’t exhibit OO behavior. That can be bad when a programmer’s mind is in OO mode.

Enter the Immutable Struct.

What if the Point struct were immutable? The equivalent code would end up looking like this:

this.Location.GetOffsetPoint(100, 0);

It’s a bit more obvious here that the code is incorrect, even to the novice programmer. We can tell that we aren’t changing anything. All we’re doing is calculating a value and promptly discarding the result. The correction is simple:

this.Location = this.Location.GetOffsetPoint(100, 0);

Of course, for an immutable struct to achieve it’s goal, it’s semantics must be obvious. Failure in this respect only causes new confusion. Look at DateTime.AddDays.This method returns a new DateTime object without modifying the original, but that’s easy to miss:

// What's wrong with this picture?
DateTime theDate = DateTime.Today;
theDate.AddDays(1);
Console.WriteLine("Tomorrow will be {0}", theDate.ToShortDateString());

You would expect tomorrow’s date to be shown, which is not the case. Were the DateTime type mutable , the code might work as expected. But that doesn’t necessarily mean that DateTime should be mutable. It shows that it’s not really about mutability at all.

The Theoretical Argument

At the extremes, there are two philosophical perspectives that can be taken on struct mutability: that all structs are mutable, and that all structs are immutable. Understanding these perspectives puts you in a good frame of mind to consider arguments for and against mutable structs. I’m going to examine these philosophies in terms of the simplest struct of all, int, and then broaden the concepts to include all structs.

The int Is Immutable

A common assertion is that an integer is immutable. It goes something like this: suppose we have the integer, 99. 99 is 99, and will always be 99, and will never be anything else. If you have a variable containing 99 and then assign 100 to the variable, you aren’t changing the 99, you are replacing it with a different integer: 100. They can’t be changed. The variable that contains the integer is mutable. The actual integers aren’t. The argument makes sense.

The int Is Mutable

Let’s look at a simple bit of code:

i = -i;

Does this code mutate i to make it negative, or does it replace it with a different integer that is its negative counterpart? (If you feel compelled to argue that the assignment operator supports the latter, understand that other operators could have been used, too, such as the increment operator. It’s just easiest to make this point with negation, which happens to require an assignment operator.)

If we consider the value of an integer to be a composition of a number and a sign, negating the integer changes the sign, but not the number. We’ve just mutated the value to produce a different value. This might not ring true in terms of two’s complement or the mechanics of a CPU, but those are implementation details. In theory, and in effect, we are mutating the integer. If you can’t bring yourself to accept this as a mutation, ask yourself, by what logic is this not a mutation? And then what’s the conclusion you reach when you apply the same logic to any other struct? (We will get there.)

The Struct Is Mutable

There’s not much to discuss here. Given a Point object, we can modify either it’s Point.X or Point.Y property, changing only a portion of the object without replacing it, and thus mutating it.

The Struct Is Immutable

This is the one you probably haven’t heard. This can be a consistent, supportable philosophy. The logic is the same as that of the immutable integer. Just as 99 is 99 and will always and only be 99, the Point (5, 5) is (5, 5), will always be (5, 5), and will never be a different Point. The variable is mutable. The object is not.

Of course, as mentioned, we can modify the Point without replacing it. This is our very definition of mutable. But if we change our Point.Y property to, say, 10, we have the Point (5, 10). That’s a different point. (5, 5) is distinct from (5, 10) just as much as -1 is distinct from +1. If the int is immutable, so is the Point, and so is any struct.

So, Which Is It?

Either perspective is valid, and consistent with what we see. They’re different ways of looking at the same thing.

What’s with the double-think? .NET allows us to treat structs like objects in many ways, but at the end of the day, a value type has distinct differences from real objects. Separate variables holds separate values in separate memory locations. We create the paradox when we try to treat them like objects. Either any two equal values can be considered the same object (99 is 99), or every value can be considered a separate object. This duality doesn’t really jive with object-oriented philosophy, and the argument over struct mutability is an attempt to reconcile the paradox.

In this theoretical context, we haven’t established whether a struct should or shouldn’t be mutable. It has become moot, because what we have established is that a struct is both mutable and immutable. That really doesn’t sound like a useful conclusion (it isn’t). So what’s the point? It helps the discussion of whether structs should be mutable when we understand what mutability really means: a little less than we thought, and a matter of perspective. All that’s left to discuss is what we want our code to look like.

Back To Reality,

because we haven’t really nullified the concept of mutability. A DateTime is immutable and a Point is not, and this is still a meaningful statement. But it’s a statement about how we will write our code:

DateTime theDate = DateTime.Today;
theDate = theDate.AddDays(1);

Point thePosition = new Point(5, 5);
thePosition.Offset(5, 0);

Both bits of code do something very similar. They just look different.

The Immutable Cause

The mutable struct creates a conflict: non-objects in an object-oriented language. Value-type semantics can contradict our expectations when we are thinking in object-oriented terms. It can become difficult to reason about what will happen with the code, and you end up having problems with things like trying to change the position of a form. In less contrived circumstances, what you have is a bug that is really hard to track down.

Immutable structs let us pretend that values and objects are the same thing. When an object is immutable, the only way to “modify” it is to create-and-replace. When a value is immutable, the only way to “modify” it is to create-and-replace. We’ve reconciled the incongruity between values and objects.

Want to change the value of a DateTime variable? You’ll have to create a new DateTime that is based on the original. This is true whether the DateTime is a value or an object. It doesn’t matter anymore. Let’s go back to our hypothetical immutable Point:

this.Location.GetOffsetPoint(100, 0);

There’s not much room for confusion here. You can’t try to modify the Point object, so there is no need to reason about what would happen if you did. We have to create a whole new Point object, which obviously won’t have any effect until it is copied back to a variable or property.

In Defense of the Mutable Struct

I’m not opposed to the idea of immutable structs. They can make code less confusing. But simpler coding does not happen automatically because a struct is immutable. Immutable structs aren’t really functionally different from mutable structs. Immutable structs don’t guarantee better or simpler code, and Mutable structs don’t guarantee worse or more confusing code. What really matters is that your types, structs or otherwise, are designed to make their behavior intuitive.

“But There Are Other Reasons To Use Immutable Types!”

It’s true! In certain situations, immutable types are easier to reason about. Even though your string is an object, it’s value can’t be changed behind your back. Good to know! Immutable types are great for multi-threading, too. But these aren’t arguments that support the notion that all structs should be immutable. If anything, they apply more to reference-types than value-types.

What About Structs That Implement Interfaces?

Here’s a good one: When is a value-type not a value-type?

interface IAdd {
    void Increment();
}

struct IntContainer: IAdd {
    public int Value;

    public IntContainer(int value) { this.Value = value; }
    public void Increment() { Value++; }
    public override String ToString() { return Value.ToString(); }
}

public void Confusion() {
    IntContainer ValueA = new IntContainer(10);
    IntContainer ValueB = ValueA;

    ValueB.Increment();
    Console.WriteLine(ValueA); // Output: 10

    IAdd ReferenceA = new IntContainer(10);
    IAdd ReferenceB = ReferenceA;
    ReferenceB.Increment();
    Console.WriteLine(ReferenceA) // Output: 11
}

This is another case of bad new caused by treating values like objects. The interface is an object-oriented concept. A value is just a value; an object implements an interface. In the wonderful world of .NET, though, structs are allowed to implement an interface. When we need to treat a value like an object, it gets boxed. It gets put in the same kind of “package” as every normal reference-type object. In most cases, we can’t use the boxed value until we un-box it by casting it back to a value type. This means that while it’s parading as an object, it never actually has the chance to act like an object. Interfaces are the exception, because they box a value type, and they declare behavior for that boxed object. Magically, our simple value has been promoted to a full-on object! This results in contradictory semantics. It’s hard to see this as a sensible behavior. (But what’s the alternative?)

This is an argument that is sometimes made for immutable structs. If a struct is not mutable, this problem can’t manifest.

The statement is true enough, but it doesn’t actually fix anything. If an interface implies mutability, the struct must be mutable. How do you implement an immutable IEnumerator? If the interface doesn’t imply mutability, then the implementation probably shouldn’t mutate the struct. The best solution is to avoid treating values like objects.

The Moral Of The Story

I’ve said it plenty of times by now. Values aren’t objects. Know the difference between the two, and design your types so that their behavior is intuitive. If this means making a struct immutable, so be it, but this doesn’t need to be a hard-and-fast rule.

Leave a Reply

Your email address will not be published.


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">