Iterative Coding

Russ Penska18/06/2020

Why I'm Excited About Immutability In C#9

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.

Simple Immutable Types in C#8

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...

"With" in C#8

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!

Equality in C#8

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...

Complete Example in C#8

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:

Immutability 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-only properties (init) in C#9

C#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!

Records (data) in C#9

The 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 Expressions (with) in C#9

Remember 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.

Record equality

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.

Summary

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.

Like 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.

Back home

Copyright © 2020 Russ Penska