Records were first introduced into .Net in version 5(C# 9). The basic premise was that, whilst classes work well, for some very typical instances of simply storing data, they work less well. Records were introduced as a specialised type of class to provide some of this functionality.
Like classes, they still reside on the Heap, and are still Reference Types. In this post, I’ll cover the key features of a record type, and why you might use them. Before that, though - let’s talk about how to declare them.
Declaration
Record types can be declared in two different ways - and, weirdly, it matters which you use. The following:
public record Song(string artist, string title);
Will declare an immutable record, that is, the following will not compile:
song1.title = "The Hammer";
That’s because, it translates to the following:
public record Song
{
public string Title { get; init; }
public string Artist { get; init; }
}
However, if you explicitly declare:
public record Song
{
public string Title { get; set; }
public string Artist { get; set; }
}
The record is not immutable, and this will compile fine:
song1.Title = "The Hammer";
For the purpose of this demo, we’ll work with the following declarations:
public record Song(string artist, string title);
public record Album
{
public string Title { get; set; }
public string Artist { get; set; }
}
So, whilst a record
isn’t immutable, it is immutable by default.
ToString()
Another neat little feature is that the ToString()
method is automatically overridden to show the contents of the class, rather than a description of the object; for example:
var song1 = new Song("Motorhead", "Bomber");
Console.WriteLine(song1);
Outputs:
Song { artist = Motorhead, title = Bomber }
We’ll be using this feature for the remainder of the post to show the contents of each instance of the record.
Equality
A class is determined equal to another class if it references the same section of memory (i.e. they need to be the same instance of a class). This makes a lot of sense in programming terms, but practically, it’s useless. What, therefore, happens, is people override the Equals
method - similar to this:
public class Song
{
. . .
public override bool Equals(object obj)
{
if (obj is not Song other) return false;
return Title == other.Title && Artist == other.Artist;
}
}
But that’s about 95% of cases*!
*88.3% of statistics are made up on the spot
Records implement this by default; for example:
var album = new Album() { Artist = "Metallica", Title = "Metallica" };
var album2 = new Album() { Artist = "Metallica", Title = "... And Justice For All" };
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album2.Title = "Ride the Lightning";
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album2.Title = "Metallica";
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
If you run this, you’ll see that the equality works as you would expect it to - comparing each value, rather than the pointers that reference the class instances.
Copying Records
Records allow copying, again, in a more intuitive fashion. The basic assignment still works the same; for example:
album2 = album
Still links the two instances (i.e. a change to either now effects the other because they both now point at the same area of memory). However, the with
operator allows you to clone:
album = album2 with {};
This creates a new instance of the Album record and copies the records across.
An example of this:
var album = new Album() { Artist = "Metallica", Title = "Metallica" };
var album2 = new Album() { Artist = "Metallica", Title = "... And Justice For All" };
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album = album2 with {}; // album is copied, but it still separate memory area
album2.Title = "Ride the Lightning";
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album2.Title = "Metallica";
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
This is also useful as you can change a specific variable as you copy; for example:
var album = new Album() { Artist = "Metallica", Title = "Metallica" };
var album2 = new Album() { Artist = "Metallica", Title = "... And Justice For All" };
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album = album2 with { Title = "Master of Puppets" };
album2.Title = "Ride the Lightning";
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
album2.Title = "Metallica";
Console.WriteLine("-");
Console.WriteLine(album);
Console.WriteLine(album2);
if (album == album2) {
Console.WriteLine("Album == Album2");
}
Conclusion
If you’re using classes to only hold data, you may find that records allow you to type a lot less code and get the functionality that you might expect.