Simple immutable objects

Andrew Arnott

We’re all familiar with immutable collections now. But immutability is only as immutable as it is deep. And an immutable collection of mutable objects may not provide the depth you’re looking for. So how can one create an immutable object?

Suppose you would define the mutable version like this:

public class Fruit { 
    public string Color { get; set; }
}

An immutable version might be defined like this:

public class ImmutableFruit {
	private readonly string color;
	
	public ImmutableFruit(string color) {
		this.color = color;
	}
	
	public string Color {
		get { return this.color; }
	}
}

Now that’s fine for very simple objects. But once objects have several fields on them, the code you have to write to create a new object with just one or two fields changed gets tedious to write and maintain. So we can add a helper method for each property, that returns a new object with just that property changed. The method name convention is WithPropertyName.

We can make a couple more enhancements though. For classes with many properties, if you need to change several properties at once, allocating a new object with each property change as an intermediate step is wasteful and can contribute to GC pressure. So we also add a With method that takes optional parameters for every property found on the class, allowing bulk property changes. Finally, for scenarios where you have several changes to make to the object but wish to do so in a multistep fashion (or just prefer property setters to With- method calls), we can construct a Builder class that allows mutation, and then can return the immutable copy when you’re done. This pattern is very similar to String and StringBuilder in the .NET Framework, and also like the recent immutable collections mentioned earlier.

Another enhancement we can make is to make all constructors private. Why? Because a public constructor will end up allowing callers to allocate many copies of the default instance. Since these are immutable, this is just wasted memory and GC pressure. Alternatively, a static Default property that returns what a public default constructor would have but sharing the instance with all other callers, alleviates the pressure while also putting the caller in the “mood” for functional programming.

What does the resulting immutable object look like? Considering it’s just a simple immutable object with one property, it’s rather large.

public partial class Fruit {
	[DebuggerBrowsable(DebuggerBrowsableState.Never)]
	private static readonly Fruit DefaultInstance = new Fruit();
	
	[DebuggerBrowsable(DebuggerBrowsableState.Never)]
	private readonly System.String color;
	
	/// <summary>Initializes a new instance of the Fruit class.</summary>
	private Fruit()
	{
	}
	
	/// <summary>Initializes a new instance of the Fruit class.</summary>
	private Fruit(System.String color)
	{
		this.color = color;
		this.Validate();
	}
	
	public static Fruit Default {
		get { return DefaultInstance; }
	}
	
	public System.String Color {
		get { return this.color; }
	}
	
	public Fruit WithColor(System.String value) {
		if (value == this.Color) {
			return this;
		}
	
		return new Fruit(value);
	}
	
	/// <summary>Returns a new instance of this object with any number of properties changed.</summary>
	public Fruit With(
		System.String color = default(System.String),
		bool defaultColor = false) {
		return new Fruit(
				defaultColor ? default(System.String) : (color != default(System.String) ? color : this.Color));
	}
	
	public Builder ToBuilder() {
		return new Builder(this);
	}
	
	/// <summary>Normalizes and/or validates all properties on this object.</summary>
	/// <exception type="ArgumentException">Thrown if any properties have disallowed values.</exception>
	partial void Validate();
	
	public partial class Builder {
		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		private Fruit immutable;
	
		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		private System.String color;
	
		internal Builder(Fruit immutable) {
			this.immutable = immutable;
	
			this.color = immutable.Color;
		}
	
		public System.String Color {
			get {
				return this.color;
			}
	
			set {
				if (this.color != value) {
					this.color = value;
					this.immutable = null;
				}
			}
		}
	
		public Fruit ToImmutable() {
			if (this.immutable == null) {
				this.immutable = Fruit.Default.With(
					this.color);
			}
	
			return this.immutable;
		}
	}
}

Of course adding more properties will be common, and the code will increase a bit more with each one. Since each added property means two new fields (one for the immutable type and one for the Builder), two new properties, and one new method and several other methods to update, this gets quite tedious to write and maintain.

So what can we do to simplify immutable programming? After all, immutable programming is supposed to be easy, right? Why can’t authoring immutable types in C# be as easy as mutable types (or immutable types in F#)? There is a better way, using Visual Studio T4 templates.

Consider this simple definition of a mutable type:

class Fruit {
    string color;
}

We can leverage code generation to produce an immutable version of this class that fulfills all the patterns described above. And due to the built-in support for T4 in Visual Studio, the experience can be just as easy as maintaining the above simple mutable type. Adding a property to the Fruit class is as simple as adding one line for the field in the above mutable type. Saving the updated file will immediately update the code generated immutable Fruit class, allowing you to get Intellisense and compile against the immutable version. T4 is built into Visual Studio, so it will already work for you today. Because code generation happens at design-time, you don’t need any additional support for it in any customized build system you may have.

I’ve published the code generation tool in source form as a sample. You can see a live example of the before and after code generation. The README in that project describes more and provides instructions for incorporating this tool into your source projects.

I’d like to add features as we go so your feedback is welcome.

0 comments

Discussion is closed.

Feedback usabilla icon