In the last of our C# Futures series, we look at proposal 159, which would add compiler support for immutable classes. While it has always been possible to create immutable types in C#, and C# 6 makes is even easier, there is currently no way to say “this class is immutable” and have the compiler verify the claim.
This might not seem like it is important, as it is usually not very difficult to manually inspect the class. But without some sort of claim to immutability, it can be hard to know the developer’s intentions. Application developers may make certain assumptions, like the class is safe to use in a multi-threaded context, only to discover that in version 2 the library author added a setter or other mutable, non-thread safe feature.
Proposal 159 would add an “immutable” keyword or attribute that explicitly says the type cannot be mutated in any way. Furthermore, an immutable object can only reference other immutable objects.
Immutable objects would also have restrictions placed on their constructor. Specifically, they can’t call methods using the “this” variable as that could ‘leak’ the object before it was fully constructed, thus breaking the promise of immutability. Whether such leaking should be an error or just a warning is debatable. Sam Harwell writes,
I would prefer this be a warning. While unlikely and generally not recommended, it's hard to state deterministically that no one will need to be able to write code like this.
Immutable vs Pure
At first glance it may seem like Immutable and Pure do the same thing, but there are some important differences.
- A Pure object can have a reference to an object not marked as Pure. As mentioned before, Immutable objects cannot.
- A Pure method cannot visibly change the state of anything. That includes both the current object and any parameters.
- A Pure method or property can change internal state. For example, it can cache the result of a calculation to be reused later. This doesn’t violate the promise because outside observers can’t see the change. Again, an immutable object doesn’t have this option.
- A method on an Immutable object only promises that object won’t be changed. It is allowed to modify the state of any parameters.
Given these differences, you would expect many objects to be both Immutable and Pure.
Generics and Immutable
Generic types would be able to support Immutable. Doing so usually require each type argument to have also have an immutable constraint. This rule is, however, debatable. Some think that you can infer immutability on the type arguments without have to explicitly list them out.
Cheating
It is recognized that there will be times when you need to cheat the type system. For example, an ImmutableArray is a wrapper around a normal array. Under the basic rules of this proposal that wouldn’t be allowed. In order to address this use case, you would be able to further annotate the class to say that you are consciously violating the rule and have thought through the repercussions. This is akin to use the “unsafe” keyword, which was actually proposed for this role.
Since “unsafe” already has another meaning, other keywords are being considered. So far “mutable” is the top contender, but no one is particularly happy with it either.
readonly – implicit or explicit
In an immutable object, every field has to be semantically readonly. Some developers think this should be implied, which would make the code less verbose. Others think that, like static in a static class, every field should be explicitly marked.
Internal or Externally Verified
An important thing to consider is where immutability will be enforced. Some are arguing that, like Code Contracts and the Pure attribute, that is should be handled by an external analyzer. This would allow teams to decide whether or not they care about programmatically enforcing immutability.
Other developers are arguing that is exactly why it shouldn’t be handled by an external tool. They want the compiler to make the checks, which would ensure that it couldn’t be accidentally turned off.
Jared Parsons of Microsoft writes,
I don't think an analyzer is the right solution here though. Analyzers are great at enforcing a set of rules, or even to a degree a dialect of C#, within a single C# project. I control the compilation I can pick what analyzers I want to use.
Analyzers are less effective when there is a need to enforce rules across projects. In particular when those projects are owned by different people. There is no mechanism for enforcing that a given project reference was scanned by a particular analyzer. The only enforcement that exists is a hand shake agreement.
This would be a different promise that the Pure attribute, which only says that an object has no methods or properties that can “make any visible state changes”.