Record Types in C# 9.0
What are Record Types?
Record Types are a popular feature of F#, also commonly found in other functional languages, that have recently been introduced to C# in version 9.0 (released alongside .Net 5).
On the face of it they provide a shorthand for creating classes, often used for basic models / DTOs (Data Transfer Objects) / POCOs (Plain Old CLR Objects).
However they come with some extensive benefits:
- Lightweight syntax to definite a type
- Standardised and clean string output of all values within a record type
- Immutable, all properties are readonly by default
- Equality is value based, checks based on the values inside the type, not the reference.
- Can be deep cloned using the WITH keyword
- Can be deconstructed like Tuples
Below we'll explore these benefits in more detail
Using Record Types
Prerequisites
You'll need .Net 5.0, details on how to set it up here
Creating a Record Type
For our example we have two enums set up that the models will use:
public enum Colour { Black, Blue, Red, Green }
public enum PenState { Up, Down }
There are two ways to create a Record Type in C#, firstly we have the longhand approach:
public record Animal
{
public string Name { get; }
public Animal(string name) => Name = name;
}
This gives you more control over the Constructor and is closer to how you would expect to create a class, it also continues to carry all the benefits we'll discuss below, at the expense of using a more verbose syntax.
The second approach is to use the Positional Record syntax:
public record Position(int X, int Y);
This generates the constructor and readonly properties for you, in the above example we'll have a type that can be instantiated with 2 properties x
and y
through the constructor in the order given above.
Record types can use inheritance like standard classes too:
public record Turtle(
string Name,
Position Position,
int Angle,
PenState PenState,
Colour Colour) : Animal(Name);
In this example we are building a type that passes the Name
property down to the Animal
record type.
Instantiating a Record Type
Record Types are instantiated like any standard class. The thing to note is that the constructor is auto generated for you in the order you supply the properties when using Positional Record syntax to declare the type:
var position = new Position(10, 20);
var turtle = new Turtle("Mr Scruff", position, 90, PenState.Down, Colour.Green);
Benefit 1: Standardised & Clean String Output
Record Types when converted to a string produce a string containing all the nested values, this can make debugging and logging easier from the get go
Console.WriteLine(turtle);
/* Outputs:
* "Turtle {
* Name = Mr Scruff,
* Position = Position { X = 10, Y = 20 },
* Angle = 90,
* PenState = Down,
* Colour = Green }"
*/
Benefit 2: Immutability
The properties contained within a Record Type are immutable by default, meaning they are set to readonly and can only be assigned through the constructor.
This encourages the use of immutability in your application, simplifying the behaviour and testability of code. Scott Wlaschin has an excellent post on the benefits of immutability found here
turtle.Name = "Jim"; // Will fail as the property is read-only
Benefit 3: Value Based Equality
When creating a reference type (such as a class), if you were to compare two instances of that class using obj1 == obj2
it would by default return false. This is as reference types check the location in memory of the instance as standard, not the values that contained in that type.
Normally, if you want a class to be comparable using ==
then you must override the Equals
and GetHashCode
methods to provide your own custom value based comparison.
A benefit of record types is that they do that overriding for you, allowing two instances of a record type to be compared based on the values they contain, not their references.
var otherTurtle = new Turtle("Mr Scruff", position, 90, PenState.Down, Colour.Green);
// This outputs true as the values inside each turtle are identical
Console.WriteLine(turtle == otherTurtle);
Benefit 4: Deep Copying Using With
Deep copying without just copying the reference of an object is always a challenge in C#, usually leading to manual mapping.
When you create a record type, you can use the with
keyword to copy all values into a new object, whilst still allowing you to change a subset of values.
In this example copiedTurtle
will have the same values as turtle
, however the Colour property will be modified.
var copiedTurtle = turtle with { Colour = Colour.Blue };
Benefit 5: Deconstruction
Finally, record types can have their properties extracted using deconstruction as if they were tuples.
In the first example we are ignoring all other properties just to extract the angle
from the copiedTurtle
.
In the second we are using nested deconstruction to get the x
and y
properties out of the Position
type stored in copiedTurtle
var(_, _, angle, _, _) = copiedTurtle;
var (name, (xPosition, yPosition), _, _, _) = copiedTurtle;
Console.WriteLine($"{name} is at position {xPosition},{yPosition}");
Summary
Record Types are an excellent addition to the C# language, and one I'm looking forward to seeing in the wild over time.
I believe records will help reduce the weight and mental gymnastics needed to understand more complex models, whilst encouraging immutability by default in applications.