Immutability has been getting popular these last years, especially with the rise of not only functional programming and but also JS frameworks such as React.
It’s an important concept for many reasons, but I won’t get into it in this blog post because it’s not the point. Although I would urge you to either read The Dao of Immutability or watch Jon Skeet’s The changing state of immutability in c# video, which explain it in details.
The true constant is change. Mutation hides change. Hidden change manifests chaos. Therefore, the wise embrace history.
C# 9 is trying to embrace Immutability more, by introducing a new type, record
and init
-only properties. I finally had the time to play with them, and I’m very excited for when C# 9 will be officially out!
How to try C# 9 features
I’m sure some of you would like to try the new C# 9 features, here’s how/where I do it:
- LINQPad 6 Beta: by activating the experimental .NET 5 runtime.
- SharpLab: by choosing the master Roslyn branch.
Init-only Properties
Imagine you have the following class:
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}
Now you can create instances of it:
var item1 = new Rectangle
{
Width = 10,
Height = 5
};
Which is fine, but we can change Width
and Height
whenever we want. What if we don’t want that? What if we want our Rectangle
to be immutable?
public class Rectangle
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
}
As you can see, we’ll need to make the properties read-only and add a constructor that fills them. This just adds boilerplate (a parameterized constructor) and removes the possibility to use object initializers.
The new init
keyword comes to the rescue:
public class Rectangle | |
{ | |
public double Width { get; init; } | |
public double Height { get; init; } | |
public Rectangle() | |
{ | |
// Of course you can still initialize the values in a constructor | |
Width = 10; | |
Height = 5; | |
} | |
} | |
var rect1 = new Rectangle(); // Works | |
var rect2 = new Rectangle | |
{ | |
Width = 20, | |
Height = 10 | |
}; // Works | |
rect1.Width = 30; // CS8852 Init-only property or indexer 'X.Rectangle.Width' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor. |
This means that we can only change (set) the values of Width
and Height
when instantiating Rectangle
. After that, you can’t change the values of these properties, which essentially makes our class immutable!
Records
If you want to read the official proposal, which contains the full details, visit records.md.
Init-only properties are already doing a good job to promote immutability in C#, so you might be wondering why records are such an important addition to the language.
Records are here for two main reasons:
- A lot of our classes are “data-holders”, and creating them requires a lot of boilerplate.
- Immutability can be enhanced by things like value-based equality and deep cloning, which are automatically generated for you.
Deep dive
In order to make our Rectangle
a record, all we need is to change the class
keyword:
public record Rectangle
{
public double Width { get; init; }
public double Height { get; init; }
}
Which will be turned to this by the compiler:
public class Rectangle : IEquatable<Rectangle> | |
{ | |
[CompilerGenerated] | |
[DebuggerBrowsable(DebuggerBrowsableState.Never)] | |
private double <Width>k__BackingField; | |
[CompilerGenerated] | |
[DebuggerBrowsable(DebuggerBrowsableState.Never)] | |
private double <Height>k__BackingField; | |
protected virtual Type EqualityContract | |
{ | |
[return: System.Runtime.CompilerServices.Nullable(1)] | |
get | |
{ | |
return typeof(Rectangle); | |
} | |
} | |
public double Width | |
{ | |
[CompilerGenerated] | |
get | |
{ | |
return <Width>k__BackingField; | |
} | |
[CompilerGenerated] | |
init | |
{ | |
<Width>k__BackingField = value; | |
} | |
} | |
public double Height | |
{ | |
[CompilerGenerated] | |
get | |
{ | |
return <Height>k__BackingField; | |
} | |
[CompilerGenerated] | |
init | |
{ | |
<Height>k__BackingField = value; | |
} | |
} | |
[return: System.Runtime.CompilerServices.Nullable(1)] | |
public virtual Rectangle <>Clone() | |
{ | |
return new Rectangle(this); | |
} | |
public override int GetHashCode() | |
{ | |
return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<double>.Default.GetHashCode(<Width>k__BackingField)) * -1521134295 + EqualityComparer<double>.Default.GetHashCode(<Height>k__BackingField); | |
} | |
[System.Runtime.CompilerServices.NullableContext(2)] | |
public override bool Equals(object obj) | |
{ | |
return Equals(obj as Rectangle); | |
} | |
public virtual bool Equals([System.Runtime.CompilerServices.Nullable(2)] Rectangle P_0) | |
{ | |
return P_0 != null && (object)EqualityContract == P_0.EqualityContract && EqualityComparer<double>.Default.Equals(<Width>k__BackingField, P_0.<Width>k__BackingField) && EqualityComparer<double>.Default.Equals(<Height>k__BackingField, P_0.<Height>k__BackingField); | |
} | |
protected Rectangle([System.Runtime.CompilerServices.Nullable(1)] Rectangle P_0) | |
{ | |
<Width>k__BackingField = P_0.<Width>k__BackingField; | |
<Height>k__BackingField = P_0.<Height>k__BackingField; | |
} | |
public Rectangle() | |
{ | |
} | |
bool IEquatable<Rectangle>.Equals(Rectangle other) | |
{ | |
return Equals(other); | |
} | |
} |
Here are the key things that you need to know about:
- Line 11: A generated property used in the
Equals
method to only compare two instances of the same type, so inheritance won’t matter. - Line 49: A generated
Clone
method that uses the copy constructor to clone the instance. - Line 60: A generated override of
Object.Equals
. - Line 65: A generated virtual
Equals
method that does a full value-based comparison. - Line 70: A generated copy constructor that copies the values of all the fields.
- Line 80: A generated
IEquatable<Rectangle>.Equals
that uses the generatedEquals
method, since our generated class implementsIEquatable<Rectangle>
.
Ways to define records
There are a couple of ways to define records:
// Defining a record like this | |
public record Rectangle1 { double Width; double Height; } | |
// Is equivalent to this | |
public record Rectangle1 | |
{ | |
public double Width { get; init; } | |
public double Height { get; init; } | |
} | |
// Defining a record like this | |
public record Rectangle2(double Width, double Height); | |
// Is equivalent to this | |
public record Rectangle2 | |
{ | |
public double Width { get; init; } | |
public double Height { get; init; } | |
public Rectangle2(double width, double height) | |
=> (Width, Height) = (width, height); | |
public void Deconstruct(out double width, out double height) | |
=> (width, height) = (Width, Height); | |
} |
With-expressions
Since we’re working with immutable data, a core concept is to create new object from existing one, by changing one or more properties. C# 9 adds the with
expression, which is just a syntax sugar:
void Main() | |
{ | |
var rect = new Rectangle(10, 5); | |
var widerRect = rect with { Width = 20 }; | |
var longerRect = widerRect with { Height = 10 }; | |
Console.WriteLine(rect.Equals(widerRect)); // False | |
Console.WriteLine(widerRect.Equals(longerRect)); // False | |
} | |
public record Rectangle(double Width, double Height); |
Here’s the actual generated code:
// Generated Rectangle class omitted | |
void main() | |
{ | |
Rectangle rectangle = new Rectangle(10.0, 5.0); | |
Rectangle rectangle2 = rectangle.<>Clone(); | |
rectangle2.Width = 20.0; | |
Rectangle rectangle3 = rectangle2; | |
Rectangle rectangle4 = rectangle3.<>Clone(); | |
rectangle4.Height = 10.0; | |
Rectangle rectangle5 = rectangle4; | |
Console.WriteLine(rectangle.Equals(rectangle3)); | |
Console.WriteLine(rectangle3.Equals(rectangle5)); | |
} |
As you can see, the with
expressions turn into a Clone
+ manual set of the changed properties. Note that with
works with init
properties because it uses the object initializer syntax.
Conclusion
Init-only properties and Records will boost our productivity and help us embrace immutability into our software. Personally, I can’t wait to use this in Blazor to manage state.
C# 9 is without a doubt a great milestone for the language. I only presented two features, but the version comes with a lot more! Make sure to check them out here.
Hope you enjoyed the post, see you soon!