Mutable value types are evil! Sort of...

11/6/2022
3 minute read

You might have heard that mutable value types are evil. But why is that and why does the .NET framework use them then? Are they really that evil?

Let's have a look at a few examples and have a look at what is going on!

Value types are objects, which behave like a single value. Let's take DateTime for example. DateTime itself acts like an atomic value even though it might consist of multiple parts. Guid would be another example of a value type. And all of them have a typical behavior: They are immutable. Immutable means that the state of the object can not change anymore. Once it's set, it will keep that state virtually forever. You can see that with DateTime pretty easily, as the APIs will always return a new DateTime object:

var stevensBirthday = new DateTime(1991, 5, 17);

// This will not change stevensBirthday, but will return a new DateTime instance
var oneYearLater = stevensBirthday.AddYears(1);

Pretty straightforward. So what is the issue, if we would allow a mutable value type? Let's have a small example:

var mutableDateTime = new MutableDateTime(1991, 5, 17);

Mutate(mutableDateTime);
Mutate(mutableDateTime);
Mutate(mutableDateTime);

Console.WriteLine(mutableDateTime);

void Mutate(MutableDateTime mutableDateTime) => mutableDateTime.AddOneYear();

struct MutableDateTime
{
    private DateTime internalDateTime;
    
    public MutableDateTime(int year, int month, int day) 
        => internalDateTime = new DateTime(year, month, day);
    
    public void AddOneYear() => internalDateTime = internalDateTime.AddYears(1);

    public override string ToString() => internalDateTime.ToString();
}

We create a date time object with the same date as above. We also have a Mutate function, which will increase the year by one. Now what is the expected outcome? Something along 1994, because we called the method 3 times? Well no, it is still 1991. The problem is that value types (struct) are passed by value (hence the name) so each time we get a copy and not the "original" object. Mutations are only reflected in the copy!

We can make this work, if we pass them by reference:

Mutate(ref mutableDateTime);
Mutate(ref mutableDateTime);
Mutate(ref mutableDateTime);

Console.WriteLine(mutableDateTime);

void Mutate(ref MutableDateTime mutableDateTime) => mutableDateTime.AddOneYear();

The same applies to storing mutable value types in lists:

var list = new List<MyPointX> { new() };
list[0].X = 20;

public struct MyPointX
{
    public int X;
}

The compiler will complain about that code with: "[CS1612] Cannot modify the return value of 'List.this[int]' because it is not a variable".

Why is .NET using them then?

The simple reason is often times: Performance. Have a look at List<T>, especially the way it returns GetEnumerator:

public Enumerator GetEnumerator() => new Enumerator(this);

The type is implemented as follows:

public struct Enumerator : IEnumerator<T>, IEnumerator

What is going on here? The reason is very simple: We try to avoid boxing in foreach loops. Also cleaning up the stack is super-cheapof small objects in comparison to allocating and de-allocating stuff on the heap, plus no Garbage Collector involved. The downside of this is, that the enumerator has to be mutable. I mean you would like to have also the second element in a foreach loop and not only the first one over and over again. Another thing is, that enumerators shouldn't be passed around in the first place. The >80% case is used in some loops and that is it!

Another thing is caching. structs are better suited for caching than a class object. Here is some more information about that.

The compiler can help you

To avoid mutable struct's you can use the readonly modifier, which "detects" mutations. This keyword is also allowed with record struct's: readonly record MyStruct.

Conclusion

We saw some pitfalls and reasons to use them. So the bottom line is: Prefer immutable value types over mutable ones. But in the end, there are certain use cases where they are preferred.

struct Performance

Let's have a small look into structs and how they work when using Equals and GetHashCode. Plus have a brief look into a new C# 10 feature: readonly record struct.

Pattern matching and the compiler can be surprising

Pattern matching is a powerful feature in C#. It allows you to match a value against a pattern and extract information from the value. The compiler does the magic for you - and sometimes it struckles with that.

Turn on Nullability checks by default

Since C# 8, we have nullable reference types. The word sounds odd, given the fact that reference types are always nullable. The idea is that the default is that your reference types have to be properly initialized. Here are my thoughts after a few years of using them.

An error has occurred. This application may no longer respond until reloaded. Reload x