Positional records in C# 9 - Blexin (2023)

Positional records in C# 9 - Blexin (1)

In a previous article, I have talked about the probable innovations that would be introduced with the new version of the Microsoft language C# 9.

Among these, the one that seems to be the most interesting for many developers is the introduction of Records.

A Record type provides us an easier way to create an immutable reference type in .NET. In fact, by default, the Record’s instance property values cannot change after its initialization.

The data are passed by value and the equality between two Records is verified by comparing the value of their properties.

Therefore, Records can be used effectively when we need immutability, as we need to send or receive data, or when we need to compare the property values of objects of the same type.

Before exploring the topic, let’s talk about another feature introduced with this version of the language and closely related to the Records: the init keyword, which should be associated with properties and indexers.

 public class Person { public string Name { get; init; } public string Surname { get; init; } public Person() { } public Person(string name, string surname) { Name = name; Surname = surname; } } 

Thanks to this new functionality, as the name suggests, theinitonlyproperties can be setonlywhen the object is initialized, but they cannot be changed later;in this way,it is possible to have an immutable model:

var person = new Person //Object Initializer { Name = "Francesco", Surname = "Vas" }; var otherPerson = new Person("Adolfo", "Arnold");//Constructor person.Surname = "de Vicariis"; //Compile error 

Please note: since there is a parameterized constructor in the class, the Object Initializer code fragment compiles only if the parameterless constructor is explicitly present.

Let’s go back to Records and see how to define them using the default syntax. Those Records written in this way, i.e. with a list of parameters, are called Positional Records:

public record Person(string Name, string Surname); 

Let’s see what’s behind the scenes!

Analyzing theILcode generated by this syntax, we see that the instruction is interpreted as aPersonclass that implements theIEquatable<T>interface.

The class contains two private fields of the string type and a parameterized constructor with the two related arguments of the string type:

.class public auto ansi beforefieldinit Person extends [System.Runtime]System.Object implements class [System.Runtime]System.IEquatable`1<class Person> { .field private initonly string '<Name>k__BackingField' .field private initonly string '<Surname>k__BackingField' .method public hidebysig specialname rtspecialname instance void .ctor(string Name, string Surname) cil managed { string Person::'<Name>k__BackingField' string Person::'<Surname>k__BackingField' instance void [System.Runtime]System.Object::.ctor() } } 

We also see the public getter and setter methods of the properties, but if we pay attention, we note that the setter methods have the IsExternalInit attribute:

.method public hidebysig specialname instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Name(string 'value') cil managed { //...} 

This is because the keyword init, which we talked about at the beginning of the article, is used for each property of the Record declared with the default syntax:

public string Name { get; init; } 

Tolet youunderstand better,Iwill show you a file produced with anILcode analysis software (ILSpy), which interprets our definition as follows:

public class Person : IEquatable<Person> { private readonly string <Name>k__BackingField; private readonly string <Surname>k__BackingField; protected virtual Type EqualityContract { get { return typeof(Person); } } public string Name { get; init; } public string Surname { get; init; } public Person(string Name, string Surname) { this.Name = Name; this.Surname = Surname; base..ctor(); } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Person"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("Name"); builder.Append(" = "); builder.Append((object?)Name); builder.Append(", "); builder.Append("Surname"); builder.Append(" = "); builder.Append((object?)Surname); return true; } public static bool operator !=(Person? r1, Person? r2) { return !(r1 == r2); } public static bool operator ==(Person? r1, Person? r2) { return (object)r1 == r2 || (r1?.Equals(r2) ?? false); } public override int GetHashCode() { return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Surname); } public override bool Equals(object? obj) { return Equals(obj as Person); } public virtual bool Equals(Person? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<string>.Default.Equals(Surname, other!.Surname); } public virtual Person<Clone>$() { return new Person(this); } protected Person(Person original) { Name = original.Name; Surname = original.Surname; } public void Deconstruct(out string Name, out string Surname) { Name = this.Name; Surname = this.Surname; } } 

We can see that, with a single line of code, we will have:

  • Init only properties that guarantee us an immutable type instance without additional declarations.
  • A constructor with all its properties as arguments, called Primary Constructor.
  • APrintMembers()method and an override of theToString()method that provide us with a textual representation of the type and values of the object’s properties.
  • Value-based equality checks with no need to override the GetHashCode() and Equals() methods.
  • An implementation of the Deconstruct() method, which allows us to use object deconstruction to access individual properties as individual values.

Let’s take some concrete examples. We initialize a Record type object using a constructor as if we were creating an instance of a class:

var person = new Person("Francesco", "Vas"); 

It’s not possible to use anObject Initializerby defining aRecordwith the default syntax. As we saw from the IL code, the class has only the parameterized constructor.

Butinstead,aparameterlessconstructor is missing, which is necessary for its operation:

var personWithInitializer = new Person { Name = "Francesco", Surname = "Vas" }; //Compile error 

If we try to change the value of a property after the object is initialized, we get a compile error.

As we said, it is not possible to change the value of an existing instance of a record type:

person.Name = "Adolfo"; //Compile error

We can create a copy of the record instance by changing all or some of its properties:

var otherPerson = person with { Surname = "de Vicariis" }; 

In this way,we create a newotherPersonRecordof typePersonwith the same values as the existing instanceperson,except for the values we supply after thewithstatement.

If we now try to use the override of the ToString() method that the definition of the Record provides, we can verify that the results are as expected:

Console.WriteLine(person.ToString());// Person { Name = Francesco, Surname = Vas } 

Or we can simply write:

Console.WriteLine(otherPerson);// Person { Name = Francesco, Surname = de Vicariis } 

As expected, we get the textual representation of the type, property values of the two Records, and the the original record instance has been cloned and modified.

Let’s now try to compare two records:

var person = new Person("Francesco", "Vas"); var otherPerson = new Person("Francesco", "Vas"); Console.WriteLine(person.Equals(otherPerson)); //Returns True Console.WriteLine(person == otherPerson); //Returns True Console.WriteLine(person.GetHashCode() == otherPerson.GetHashCode());//Returns True 

Unlike a class, Records follow structural equality rather than referential equality. Structural equality ensures that two records are considered equal if their type is equal and all properties’ values are equal.

Let’s also briefly talk about the deconstructor, which is not a novelty introduced with C# 9, but is made available to us in a “free” way by the definition of the Positional Record.

As we have seen from the IL code, there is a Deconstruct method with as many parameters as the properties of the Record we have created. This allows us to access all properties individually:

var person = new Person ("Francesco", "Vas", 20); var (name, surname, id) = person; Console.WriteLine(name + " " + surname + ", Id: " + id);// Francesco Vas, Id: 20 

Or we can usediscardvariables to ignore elements returned by aDeconstructmethod:

var (_, _, onlyId) = person; Console.WriteLine(onlyId);// 20 

Each discard variable is defined by a variable named “_”, and a single deconstruction operation can include several discard variables.

Records can be a valid alternative to classes when we have to send or receive data. The very purpose of a DTO is to transfer data from one part of the code to another, and immutability in many cases can be useful. We could use them to return data from a Web API or to represent events in our application.

They can be used easily when we need to compare property values of objects of the same type. Furthermore, immutability can help us in simultaneous access to data: we do not need to synchronize access to data if the data is immutable.

What do you think of all these features in a single line of code? I find it really practical!

There would still be so much to say. For example, it is also possible to write our own Record and customize it, or that the Records support inheritance from other Records… but we will talk about it maybe another time.

See you in the next article! Stay Tuned!

Positional records in C# 9 - Blexin (2)

Francesco Vastarella


What are positional records in C#? ›

Records in C# 9.0 provide positional syntax to declare and initialize property values of an instance. The compiler makes the properties public init-only auto-implemented when the user employs positional syntax to declare and initialize properties.

What is positional records? ›

Let's go back to Records and see how to define them using the default syntax. Those Records written in this way, i.e. with a list of parameters, are called Positional Records: 1. public record Person( string Name, string Surname);

What is the difference between record and class performance in C# 9? ›

The main difference between class and record type in C# is that a record has the main purpose of storing data, while class define responsibility. Records are immutable, while classes are not.

What is record type equality in C#? ›

For records, value equality means that two variables of a record type are equal if the types match and all property and field values match. For other reference types such as classes, equality means reference equality. That is, two variables of a class type are equal if they refer to the same object.

What is positional format? ›

The position format is the way your GPS position is displayed on the watch. All the formats relate to the same location, they only express it in a different way. You can change the position format in the watch settings under Navigation » Position format.

How to use records in C#? ›

Beginning with C# 9, you use the record keyword to define a reference type that provides built-in functionality for encapsulating data. C# 10 allows the record class syntax as a synonym to clarify a reference type, and record struct to define a value type with similar functionality.

Top Articles
Latest Posts
Article information

Author: Kieth Sipes

Last Updated: 03/31/2023

Views: 5812

Rating: 4.7 / 5 (47 voted)

Reviews: 94% of readers found this page helpful

Author information

Name: Kieth Sipes

Birthday: 2001-04-14

Address: Suite 492 62479 Champlin Loop, South Catrice, MS 57271

Phone: +9663362133320

Job: District Sales Analyst

Hobby: Digital arts, Dance, Ghost hunting, Worldbuilding, Kayaking, Table tennis, 3D printing

Introduction: My name is Kieth Sipes, I am a zany, rich, courageous, powerful, faithful, jolly, excited person who loves writing and wants to share my knowledge and understanding with you.