Immutable Records could be added in C# 8.x

There are talks about this feature being postponed to a minor version after 8.0 is released, as it might not be ready for the major C# 8.0 release.

The features for C# 8.0 still hasn’t been decided yet, so that means this information is still subject to change!

Since Microsoft rebuilt the C# compiler into what is now the Roslyn compiler, the language teams’ feature implementation speed has been phenomenal, they are cranking features out in record time. 


Pro C# 7: With .NET and .NET Core

Microsoft is committed to continuously update the C# language. They have even given us incremental updates of features that were not yet ready for the major versions release date and subsequently added it in minor language updates, such as C# 7.1, 7.2 and 7.3.

Their speed is incredible and it doesn’t seem that long ago that we were introduced to C# 7, and many developers might not even have started using its features.

If you haven’t caught up on C# 7, you can take a look at this book: Pro C# 7: With .NET and .NET Core

But alas! C# 8 is on its way and has been for some time now.

The problem

If you have some years under your belt as a software developer, you know the code smell of passing primitives, like ints and strings around your system.

Value Objects keep encapsulation in place

Consider the following code:

public void NotifyUserChange(string username, int age, bool isOnline, MyEnum type)

Here we are notifying a class that the user state has changed. The user state is spread across multiple primitive values, and it is hard to tell for the caller, what the legal combinations of these values are. Meaning the method itself needs to check whether the combination of values are a legal state, which breaks encapsulation because it needs to know detailed information about what a legal user state is.

These values are obviously meant to be kept together, as they form a coherent user state. When you have more than a few (2+) primitives going into a method call, you should, in general, consider encapsulating that information into a Value Object or a Record.

Value objects improves encapsulation greatly, as they stop assignment illegal values to a state. That improves the encapsulation of the method as well, as it does not need to question the validity of the passed values. Keeping the Single Responsibility principle in place for both the the class the method lives in, and the Value Object itself.

By requiring a value object in the method call we clearly state what is required of the caller, and ensures its correctness. We also more clearly express our intentions in code, which is a nice benefit.

By using a Value object, we can change the method call to the following:

public void NotifyUserChange(UserState user)

Why isn’t this used everywhere?

Many systems have a tendency to pass primitives around and even expose them to the rest of the software, instead of encapsulating these values in meaningful value objects. Most developers are aware that is good practice to create value objects, but the amount of “boilerplate” code that is needed, often stops them from following through with their good intentions.

It is not a big issue to create value objects, you could just create it like this:

public class UserState
{
   public string Username {get;private set;}
   public int Age{get;private set;}
   public bool IsOnline {get;private set;}
   public MyEnum Type {get;private set;}

   public UserState(string username, int age, bool isOnline, MyEnum type)
   {
      this.Username = username; 
      this.Age = age; 
      this.IsOnline = isOnline;
      this.Type = type;
   }
}

This is often what developers create to pass around values. But often you want to store the value in a list, and maybe even compare it to another object, that would require you to override and implement the Equals method, but you should never override the Equals method without also overriding the GetHashCode() method.

To follow good conventions, and use these objects in Dictionaries and lists, the amount of code you have to implement becomes a bit daunting, considering you just wanted to encapsulate your values.

public class UserState : IEquatable<UserState>
{
   public string Username {get;}
   public int Age {get;}
   public bool IsOnline {get;}
   public MyEnum Type {get;}

   public UserState(string username, int age, bool isOnline, MyEnum type)
   {
      this.Username = username; 
      this.Age = age; 
      this.IsOnline = isOnline;
      this.Type = type;
   }

   public override bool Equals(object other)
   {
      if(other == this)
         return true;
      if(other is UserState == false)
         return false;
      var userState = other as UserState;

      return Username == userState.Username 
           && Age = userState.Age
           && IsOnline == userState.IsOnline
           && Type == userState.Type;
   }

   public override int GetHashCode()
   {
      return (Username.GetHashCode() * 17)
        + (Age.GetHashCode() * 17)
        + (IsOnline.GetHashCode() * 17) 
        + (Type.GetHashCode() * 17);
   }
}

Using Value Objects are becoming more and more common, and the C# team have a solution to solve the issues with all this boilerplate code.

Records to the rescue!

In C# 8 Microsoft is planning to improve this, making all of the above and more into a one liner looking like this:

public class UserState(string username, int age, bool isOnline, MyEnum type)

This one line of code will give you the following:

  • Immutable class encapsulating your values.
  • Equals() and GetHashCode() methods
  • Deconstructor for tuple deconstruction
  • With function that enables you to change a value and get a new Record in return.

Which will translate into the following code:

public class UserState : IEquatable<UserState>
{
   public string Username {get;}
   public int Age {get;}
   public bool IsOnline {get;}
   public MyEnum Type {get;}

   public UserState(string username, int age, bool isOnline, MyEnum type)
   {
      this.Username = username; 
      this.Age = age; 
      this.IsOnline = isOnline;
      this.Type = type;
   }
   public bool Equals(UserState other)
   {
      return Username == other.Username 
           && Age = other.Age
           && IsOnline == other.IsOnline
           && Type == other.Type;
   }

   public override bool Equals(object other)
   {
      return (other as UserState)?.Equals(this) == true;
   }

   public override int GetHashCode()
   {
      return (Username.GetHashCode() * 17)
        + (Age.GetHashCode() * 17)
        + (IsOnline.GetHashCode() * 17) 
        + (Type.GetHashCode() * 17);
   }

   public void Deconstruct(out string Username, out int Age, out bool IsOnline, out MyEnum Type)
   {
      Username = this.Username;
      Age = this.Age;
      IsOnline = this.IsOnline;
      Type = this.Type;
   }
   
   public UserState With(string Username = this.Username, int Age = this.Age, bool IsOnline = this.IsOnline, MyEnum Type = this.Type)
   {
      return new UserState(Username, Age, IsOnline, Type);
   }
}

You can see that we actually get a lot of features for this one line of code. And I believe I will be using this heavily once it is released.

Examples of usage

Deconstructing a Record

If you ever need the original primitive values back, you can just use the deconstructor, which was introduced in C# 7 together with ValueTuples.

var userState = new UserState("Snede", 28, true, MyEnum.Value);
var (Username, Age, IsOnline, Type) = userState;

Console.WriteLine($"Hello I am {Username} and I am {Age} years old"); // Writes Hello I am Snede and I am 28 years old

Change a records value

Since records are immutable, meaning you can never change the value, only ever create a new instance with new values, it comes with the With method, which is very common in immutable code.

Side note: Being immutable is a fantastic feature, it means that the object is thread-safe as it can never change(mutate) after it has been created, and therefor no other threads or parts of the software can change the values of that specific instance.

var userStateOriginal = new UserState("Snede", 28, true, MyEnum.Value);
var userStateOlder =  userStateOriginal.With(Username: "SnedeOlder", Age; 29);

Console.WriteLine($"Hello I am {userStateOriginal.Username} and I am {userStateOriginal.Age} years old"); // Writes Hello I am Snede and I am 28 years old

Console.WriteLine($"Hello I am {userStateOlder.Username} and I am {userStateOlder.Age} years old"); // Writes Hello I am SnedeOlder and I am 29 years old

As you can see from the above example, when we “change” the value, we actually get a new instance, and the original instance stays the same.

I hope you are as excited about the upcoming C# 8 features, as I am.

// André Kock

References:

Leave a Reply

Your email address will not be published. Required fields are marked *