Skip to main content

C# 9 — Records

·5 mins
C# 9 — Records

In the last post where I went through 3 New Features of C# 9 I skipped Records because they deserve a dedicated post, as there is a lot of things that we need to go through, so let get started.

What are records according to Microsoft’s official documentation ?

Records are a reference type that provides synthesized methods to provide value semantics for equality. Records are immutable by default.

If you are not familiar with how types are classified in .NET here is a quick intro. Note that it’s very important to understand these concepts as you advance in your programming using C#, but for now you can just try to get the high level concept.

Reference Type vs Value Type: #

Value Types holds data value within it’s memory space. For example int i = 2; , the value 2 is going to be stored in the memory space allocated for the variable i

When you pass a value-type variable to a method, the system creates a copy of the original variable, and if ever you change the value of the variable inside the method the original variable is not going to be affected.

using System;

int i = 2021;
Console.WriteLine($"Initially {i}");
SetToZero(i);
Console.WriteLine($"After Method {i}");

void SetToZero(int x)
{
    x = 0;
    Console.WriteLine($"Inside Method {x}");
}

Output:

Initially 2021
Inside Method 0
After Method 2021 // The variable i remains unchanged

Reference Type on the other hand does not store its value directly. Instead they hold an address to an other memory location where the value is stored. For example string name = "Bleu"; the system will store the value "Bleu" in a random location, and store the address of that location as the value of the variable name.

That’s why when you pass a reference-type variable to a method, the method can change the value as it has the address to the location where the value is stored.

using System;

Article article = new() {Year = "2021"};
SetToZero(article);
Console.WriteLine($"After Method {article.Year}");

void SetToZero(Article x)
{
    x.Year = "0000";
}

class Article
{
    public string Year { get; set; }
}

Output:

After Method 0000

This animation summarizes what happens when you pass value types or reference type to a method:

Value Type vs Reference Type

You can check the full list of Value types and Reference Types from Microsoft docs.

Immutable Types #

To keep it short, immutable types are types that cannot change their internal state once initialized.

An example from the .NET framework is System.DateTime structure, when you call .AddDays() method on a an already initialized instance you get a new instance and the original one remains immutable.

Records #

I like Tim Corey simple definition A Record is just a fancy class that has some pre-built code. They are a new way of defining value types, which means that we can already write the same functionalities with C# 8 it’s just that we had to write a lot of boiler plate code that is necessary but repeatable.

To illustrate this let’s look at this sample from a point of view of C# 8 developer :

Usage

class Program
{
    static void Main(string[] args)
    {
        var p1 = new Person("Elon", "Musk");

        var json = JsonSerializer.Serialize(p1);
        Console.WriteLine(json);
        var p2 = JsonSerializer.Deserialize<Person>(json);
        var isEqual = p1 == p2;
        Console.WriteLine($"p1 == p2 : {isEqual}");

        var (firstName, lastName) = p1;
        Console.WriteLine($"{lastName}, {firstName}");
    }
}

Definition

class Person : IEquatable<Person>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }

    public static bool operator ==(Person left, Person right) =>
            left is object ? left.Equals(right) : right is null;

    public static bool operator !=(Person left, Person right) =>
        !(left == right);

    public override bool Equals(object obj) =>
        obj is Person p && Equals(p);

    public bool Equals(Person other) =>
        other is object &&
        FirstName == other.FirstName &&
        LastName == other.LastName;

    public override int GetHashCode() =>
        HashCode.Combine(FirstName, LastName);
}

As you can see we had to write 34 lines of code just to get a basic type with two fields FirstName and LastName.

With record we can define the same type with the same functionalities with less code, in this case one line of code instead of 34!

record Person(string FirstName, string LastName);

If we run this code we get exactly the same output as before.

What records help us to do is basically take care of all the boiler plate that we had to write just to give the type the basic functionalities: Construction, Deconstruction, and Equality

When working with immutable data, a common pattern is to create new values from existing ones to represent a new state. For instance, if our person were to change their last name we would represent it as a new object that’s a copy of the old one, except with a different last name. This technique is often referred to as non-destructive mutation.

var person = new Person { FirstName = "Elon", LastName = "Musk" };
var otherPerson = person with { LastName = "Stark" };

The with-expression works by actually copying the full state of the old object into a new one, then mutating it according to the object initialiser. This means that properties must have an init or set accessor to be changed in a with-expression.

If you want to investigate Records in more detail, I suggest you have a look at this article on Microsoft’s blog, which goes into more detail about caller-receiver parameters, with-expressions, declaration syntax and much more.