Recently I've found myself using a lot of immutable classes in my code. This is great for lots of reasons, but there can be a lot of "boilerplate" code to write - particularly for classes with a lot of properties on them.
Luckily, C#9 looks set to make this much, much simpler to do with two of it's new features:
I'm getting this from a fantastic blog post by @MadsTorgersen which you can find here. He covers the new language features around immutability, and plenty more.
Let's take a look at how immutability looks in C# right now.
Right now (in C#8) if you want an immutable class it would look something like this:
public class Cat
{
public Cat(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; }
public int Age { get; }
}
Now anybody can create an instance of Cat
by calling the constructor, but they can't mess with any of the properties of the Cat
they create. We're not quite done yet though...
That's all good, but what if you want to change the cat's name? Well, the way to do that is to create a brand new Cat
with the same name and copy across all of the other properties. Something like this:
// creating our original cat
var felix = new Cat("Felix", 3);
// changing the name, but keeping the age
var penelope = new Cat("Lady Penelope", felix.Age);
We can also add this as a method to our Cat
instance, to make it easier for anyone wanting to "change" a cat's name. That's where "with" comes in.
public class Cat()
{
...
public Cat WithName(string name)
{
return new Cat(name, this.Age);
}
}
This makes our client code a lot nicer looking, and also really easy to understand at a glance. It looks like this:
var felix = new Cat("Felix",3);
var penelope = felix.WithName("Penelope");
But now what if someone wants to change the other properties? In our case that isn't too bad - we can just add a Cat WithAge(int age)
method and we're done. But if you have a lot of properties this quickly becomes a pain. Even worse, if somebody adds an entirely new property to Cat
they also have to update all of the With
methods as well!
So far we have immutable properties (nobody can mess with our cat) and With methods to make it simple to make very similar cats, but we're still missing something. Right now, this code won't do what we expect:
var felix = new Cat("Felix", 3);
var alsoFelix = new Cat("Felix", 3);
if (felix == alsoFelix) // it doesn't!
{
...
}
In the example above, because these are classes, felix
is not equal to alsoFelix
even though they are practically identical. This is because C# by default does a reference equality check - i.e. are these the same object in memory? And in this case they're not.
For immutable types this is counter-intuitive. You're essentially treating them as unchangeable values (much like a string
) and logically it makes sense that two identical values would be seen as equal.
No problem though - we can fix that!
public class Cat
{
...
public override bool Equals(object obj)
{
Cat other = obj as Cat;
if (other == null)
{
return false;
}
return this.Name == other.Name
&& this.Age == other.Age;
}
}
Perfect - now equality checks will work how we want them to (felix == alsoFelix
).
But... we've had to write a lot more boilerplate code, and we're not done yet! Your IDE will probably complain that you have an override for Equals
, but you're don't have one for GetHashCode
...
Here's the full, immutable Cat
in all it's boilerplate glory:
public class Cat
{
public Cat(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; }
public int Age { get; }
public Cat WithName(string name)
{
return new Cat(name, this.Age);
}
public Cat WithAge(int age)
{
return new Cat(this.Name, age);
}
public override bool Equals(object obj)
{
Cat other = obj as Cat;
if (other == null)
{
return false;
}
return this.Name == other.Name
&& this.Age == other.Age;
}
public override int GetHashCode()
{
// probably not the best implementation of this!
return this.Name.GetHashCode();
}
}
And that's only for two properties! This class is lovely to work with, but writing the class itself isn't too much fun. Let's take a look at how we can get the same functionality in C#9:
Here's what an immutable cat looks like in C#9:
public data class Cat
{
public string Name { get; init; }
public int Age { get; init; }
}
... and that's it!
We're using two new feature here - let's break it down:
init
) in C#9C#9 is going to allow "init-only" properties. That's these lines:
public string Name { get; init; }
public int Age { get; init; }
The init
keyword here means that this property may be set when the object is initialized. So this would be valid:
var felix = new Cat { Name = "Felix", Age = 3 };
But if you later tried to make a change to the Name
field the compiler won't let you:
felix.Name = "Penelope"; // this won't compile!
The behaviour is similar to get-only properties (i.e. { get; }
) , where you can only set the property inside a constructor.
What's nice about using init-only properties (i.e. { get; init; }
) instead is that clients can use the object initializer (as above) syntax to set these properties on creation. In short, you don't have to write the constructor code!
data
) in C#9The other keyword that's unusual in our C#9 example is data
, from the class declaration:
public data class Cat
This is used to tell the compiler that our class is immutable. It's a record. Why's that useful? Well, records will have some really interesting features built in.
with
) in C#9Remember how we had to write all those WithSomething
methods for our C#8 Cat
class earlier? Well, with C#9 that's all done for you. Here's an example of it in use:
var felix = new Cat { Name = "Felix", Age = 3 };
var penelope = felix with { Name = "Penelope" };
Without any extra code it's now easy to copy one of our cats and "change" a few properties.
This is my favourite part. By default, if your class is a record, equality checks will be value-based. That means that we don't have to override Equals
or GetHashCode
any more!
var felix = new Cat { Name = "Felix", Age = 3);
var alsoFelix = new Cat ( Name = "Felix", Age = 3);
if (felix == alsoFelix) // it does! by magic!
{
...
}
All of the properties will be compared, and if they're all the same then the records are considered to be equal.
In summary, C#9 looks set to take a lot / all of the boilerplate out of creating immutable classes. That means much less typing, no change for bugs to sneak into your boilerplate code, and a clear "correct" way to make a class immutable.
You no longer have to worry that you forgot to override Equals, or think about how you're supposed to override GetHashCode.
Most importantly the new features make the intent of the code much clearer.
init
tells you that this property can be chosen by the client, but it should never changedata
tells you that the class is immutable, and can be thought of as a valueLike I mentioned at the start of the post I'm getting all of this from Mads' post of new C#9 features.
His post goes into more detail on these immutability features, and shows off some other new features as well. He also happens to be the lead designer of C#, so he knows what he's talking about!
We're @iterativecoding on Twitter if you have any comments or feedback.