Records in C#
Photo by Eric Krull on Unsplash

With C# 9, we had record types introduced to the language, later the record types had some changes in C# 10 which I will explain later in another article.

What’s the Record Type

Record is a class with default implementations for some of the functionalities to make developers day to day life easier. These are mainly everything related to equality like Equals(Object),GetHashCode(),ToString() methods and == and != operators. With this new type (Record), we have an easier life comparing the properties of two different objects of the same type. It also provides the possibility to create immutable objects and some more synthetic sugars.

How To Create a Record

With record types, you can create immutable objects. Immutable means, you can’t change the values after creation, and that makes the object thread-safe. You can also make them non-immutable if you don’t need the functionality.

This first example is the way you create a non-immutable record:

Student sam = new Student()
{
    FirstName = "Sam",
    LastName = "Adam",
    Age = 33
};

// It's ok to do change the LastName property since it's not immutable 
sam.LastName = "Fadam";

public record Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

With the following two ways you can make your record immutable:

You can use Positional Parameters to create your record with the minimum amount of code.

Student sam = new("Sam", "Adam", 33);

public record Student(string FirstName, string LastName, int Age);

Or, you can use the init-only setters to make it immutable.

Student sam = new Student()
{
    FirstName = "Sam",
    LastName = "Adam",
    Age = 33
};

public record Student
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public int Age { get; init; }
}

Just pay attention if you create your record using the Positional Parameters approach then you can create the object with a constructor and if you make the record using the No Positional Parameters then you can initialize the object using an object initializer.

If you try to change the property after making it immutable you will get a compiler CS8852 error like the picture below.

Copying A Record Using “with” Expression 

The reason you would need to have an immutable record is for scenarios when you don’t want to allow changes to the object while when you really need a changed version you can make a copied version of the object with the changes you need.

Using the with keyword you can make a new instance of a record with changed properties and that’s much easier than cloning a class object.

Student adam = new("Adam", "Dan", 22);
var sam = adam with { FirstName = "sam" };
Console.WriteLine(sam);

public record Student(string FirstName, string LastName, int Age);

Result:

Value Types vs Reference Types

To understand the equality features introduced with records clearly, you need to know the differences between value types and reference types; therefore, I will explain that before moving to that topic. If you already know the difference you can skip this section.

C# objects in memory are being managed in two different groups value types and reference types. Value types are being stored in the stack memory directly but the reference types are stored in the heap memory with a reference (address) to the object stored in the stack memory.

How Does Class Equality Work?

As I explained, we have value types and reference types and they are being managed differently in the memory. A class with the default implementation compares the values stored in stack memory (the address) and that will only be true when you compare it with the same reference (address) in the stack memory, in simple words it will be only true if you compare an object with itself. That means if you compare two instances from a class with completely identical properties it will return false because they are pointing to different objects in the heap memory.

In the following image, the variables are pointing to the same object in the heap so y == z will be true.

In the following image, the variables are pointing to different objects in the heap memory with the same values assigned to the properties (identical objects), but the y == z will be false.

How Does Record Equality Work?

Now that we know how equality works in class, let’s take a look at the record and see what makes it different from a class. As I mentioned before record is a class, so the compiler makes a class and implements the IEquatable interface against the class under the hood for your record. If you want to see exactly how the class would look like you can take a look at it here for yourself.

Following is the code that is generated for my class Equals method and changes the regular class behavior when we compare two records. It compares every property with its counterpart instead of just checking the reference in the stack memory.

[System.Runtime.CompilerServices.NullableContext(2)]
public override bool Equals(object obj)
{
    return Equals(obj as Student);
}

[System.Runtime.CompilerServices.NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(Student other)
{
    return (object)this == other || ((object)other != null &&
           EqualityContract == other.EqualityContract &&
           EqualityComparer<string>.Default.Equals(< FirstName > k__BackingField, other.< FirstName > k__BackingField) &&
           EqualityComparer<string>.Default.Equals(< LastName > k__BackingField, other.< LastName > k__BackingField) &&
           EqualityComparer<int>.Default.Equals(< Age > k__BackingField, other.< Age > k__BackingField));
}

ToString() Method

Record has a nice implementation of ToString() by default which will return the object with all its properties in opposition to the class ToString() method which prints the object name if the ToString() is not overridden by the developer.

Example for Record

Student adam = new("Adam", "Dan", 22);
Console.WriteLine(adam);

public record Student(string FirstName, string LastName, int Age);

Result:

Example for Class

Student adam = new("Adam", "Dan", 22);
Console.WriteLine(adam);

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public Student(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}

Result:

These are the features introduced by record types that I found useful, I would like to hear what you enjoy or don’t like about records, and where you think is the best place to use them.

In the next article, I will focus on the changes added to the record types in c# 10 and where they can be useful.