ICYMI C# 9 New Features: Create Immutable Objects with Records

This is part of a series of articles on new features introduced in C# 9.

C# 9 introduced a new type of object that is neither a class or a struct. This new type is called a  record.

In C# 9 a record is a reference type that has value type equality semantics (more on this below).

The main purpose of defining record types is to indicate immutability for a type that is “data-centric” or in other words does not have rich behaviour (such as data transfer objects, database records, etc).

How to Define a Record in C# 9

To define a record type you use the record keyword:

record Message1
{
    public int Priority { get; set; }
    public string MessageBody { get; set; }
}

We could now create an instance and then write it to the console window:

var m1 = new Message1();
m1.Priority = 1;
m1.MessageBody = "Hi";

Console.WriteLine(m1);

This would produce the following output:

Message1 { Priority = 1, MessageBody = Hi }
Console.WriteLine automatically calls ToString() on the object passed to it, notice that we get built-in ToString() formatting support for all record types.

 

Notice in the preceding code that we are able to set Priority and MessageBody even after we have created the object – this is not immutable behaviour. To make a record immutable when declaring properties manually (see positional records below) you need to make the property setter init only:

record Message2
{
    public int Priority { get; init; }
    public string MessageBody { get; init; }
}

Now if you try and write the following code you’ll get a compiler error (“Init-only property or indexer … can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor”):

var m2 = new Message2();
m2.Priority = 2;
m2.MessageBody = "Hey there!";

To create Message2 instances you now need to set the properties when you create it, for example:

var m2 = new Message2()
{
    Priority = 2,
    MessageBody = "Hey there!"
};
You can also add constructors to record types if you want to.

What Are Positional Records in C#?

Positional records are a shorthand syntax for defining C# records. Behind the scenes they create init-only properties.

We could define a message class that is essentially the same as Message2 above with the following syntax:

record Message3(int Priority, string MessageBody);

Now we could create one with the following syntax:

var m3 = new Message3(3, "Good day sir!");

Or if you wanted to be explicit:

var m3 = new Message3(Priority: 3, MessageBody: "Good day sir!");

Even though behind the scenes we’re getting init-only properties, when you define a positional record you can’t use the following syntax:

var m3 = new Message3() // error missing arguments
{
    Priority = 3,
    MessageBody = "Good day sir!"
};

You can think of positional records as a shorthand syntax that creates init only properties and a parameterized constructor automatically behind the scenes.

Equality

Records have value-like equality semantics:

Record instances in C# 9 by default are considered equal if they store the same values and are of the same record type:

var m3a = new Message3(Priority: 3, MessageBody: "Good day sir!");
var m3b = new Message3(Priority: 3, MessageBody: "Good day sir!");
var m3c = new Message3(Priority: 3, MessageBody: "BOO!");

Console.WriteLine($"m3a == m3b : {m3a == m3b}"); // Outputs: TRUE
Console.WriteLine($"m3b == m3c : {m3b == m3c}"); // Outputs: FALSE

If you tried to compare a Message3 object with a Message2 object you’ll get a compiler error.

If you want to, you can override things like Object.Equals in a record.

Note: C# 10  will be introducing record structs .

Immutability of Record Types

One thing to be aware of is that immutability of records types is “shallow” for properties that are reference types.

In other words while you can’t change the value of a value type property you can change the properties of reference type properties in a record:

var m4 = new Message4(4, new[] { "Dear sir", "Good to see you.", "Good bye." });
Console.WriteLine(m4.MessageLines[0]); // OUTPUTS: Dear sir

m4.MessageLines[0] = "Yo yo!"; // NO COMPILER ERROR
Console.WriteLine(m4.MessageLines[0]); // OUTPUTS: Yo Yo!

m4.MessageLines = new[]; // ERROR MessageLines property object reference itself IS immutable

You can create a new immutable record object based on an existing immutable instance:

var one = new Message3(Priority: 3, MessageBody: "Good day sir!");
var two = one; 

Object two is a copy of one.

Note: record copies are “shallow” - any value type properties will have the value copied, but any reference type properties will only have the reference copied. That means that 2 record instances can have reference type properties that point to the same object. You change the object they point to and both records will be “updated” with (point to) the new value (because they share the reference to the same object in memory).

If a record is immutable, you can “update” it by creating a copy of it, and update some properties as required during the “copy” – you do this using the with keyword. For example to “update” the priority of an immutable record:

var priority3Message = new Message3(Priority: 3, MessageBody: "Good day sir!");
var priority1Message = priority3Message with { Priority = 1 };

As before, if you create a copy and use with a shallow copy is still created.

Custom C# Record Output Formatting

When you declare a record, under the hood a PrintMembers method is generated. You can also provide your own:

record Message5(int Priority, string[] MessageLines)
{
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append($"P:{Priority}");
        for (int i = 0; i < MessageLines.Length; i++)
        {
            builder.Append($" {MessageLines[i]} ");
        }

        return true;
    }
}

Now the following code:

var m5 = new Message5(5, new[] { "Dear sir", "Good to see you.", "Good bye." });

Console.WriteLine(m5);

Would output:

Message5 { P:5 Dear sir  Good to see you.  Good bye.  }

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Add comment

Loading