C# Records

Introduction

C# records and record structs are relatively new. Records offer some advantages over regular classes and structs, which I will cover here, but they are essentially another way to declare them. This is another back to basics article.

We essentially use records for:

  • Entities, such as the ones we would store in a database
  • Value objects/Data Transfer Objects
  • Immutable classes

Let's now see how to work with them.

Syntax

Records are declared like this:

public record Point(int X, int Y);

Or like this, with the exact same meaning:

public record Point
{
public int X { get; init; }
public int Y { get; init; }
}

And we instantiate a record just like any other class or struct:

Point p1 = new(1, 2);
//or
var p1 = new Point(1, 2);

We can, of course, use positional or named arguments:

var p1 = new Point(Y: 2, X: 1);

It is possible to add new properties (or methods) besides the ones declared on the primary constructor, we don't need to live with just those properties:

public record Point(int X, int Y)
{
public int? Z { get; init; } //init-only
public string? Name { get; set; } //get and set public double DistanceToOrigin => Math.Sqrt(X * X + Y * Y); //get only }

Immutable by Default

Properties declared on the primary constructor on a record are init-only, meaning, you cannot change their values:

p1.X = 10; //error: init-only property X

If we, however, declare properties in the old style, we can have getters, setters, or initialisers.

Reference and Value Type Records

record is actually a class, and a record struct is a struct, meaning, they are either reference or value types. We can, of course, have nullable records too.

Limitations

A record can only inherit from another record, or Object (the default). A record struct cannot inherit from anything. Both can implement any number of interfaces. Only records can inherit from another record.

For a list of common errors when using records, please see this.

Cloning

Cloning is implemented out of the box, and but we can change some properties on the clone, this is using a with expression:

Point p2 = p1 with { Y = 3 }; //all properties are the same except Y
Point p3 = p2 with { }; //shallow clone

Deconstruction to Tuples

It is also possible to deconstruct a record to a tuple too:

var (x, y) = p1;

Hash Code and Equality

Equality (Equals) and hash code (GetHashCode) are properly implemented for all members, and are stable:

var h1 = p1.GetHashCode();
var h2 = p2.GetHashCode(); // != h1
var h3 = p3.GetHashCode(); // == h1

Comparison is always by value, every property of the record is compared: Equals and the == operator look at what's inside the record.

String Representation

The string representation of a record (ToString) includes all properties of it:

var str = p1.ToString(); //"Point { X = 1, Y = 2 }"

This raises an interesting problem: if we have a property that points to the same object, we will get an exception while calling ToString:

record Data(string Name)
{
public Data? Parent { get; set; }
}

var data = new Data("Foo");
data.Parent = data;

data.ToString(); //exception because we enter an infinite cycle

Conclusion

I find records to be a great addition; they solve the problem of us having to define equality and hashing, and are particularly good for immutable types. I also like how we can easily get a string representation of all of its contents.

Comments

Popular posts from this blog

Modern Mapping with EF Core

C# Magical Syntax

.NET 10 Validation