Original link: https://www.infoworld.com/article/3607372/how-to-work-with-record-types-in-csharp-9.html
Original title: How to work with record types in C# 9
Translation: Desert End Wolf (with Google Translate assistance)
Leverage C# 9's record type to build immutable types and thread-safe objects.
Immutability makes your objects thread-safe and helps improve memory management. It also makes your code more readable and easier to maintain. Immutable objects are defined as objects that cannot be changed once created. Therefore, immutable objects are inherently thread-safe and free from race conditions.
Until recently, C# did not support immutability out of the box. C# 9 introduces support for immutability through the new init-only properties and the record type. init-only properties can be used to make individual properties of an object immutable, while record can be used to make an entire object immutable.
Because immutable objects do not change their state, immutability is a desirable feature in many use cases such as multi-threading and data transfer objects. This article discusses how we can use init-only properties and the record type in C# 9.
To use the code examples provided in this article, you should have Visual Studio 2019 installed on your system. If you don't have it yet, you can download Visual Studio 2019 here.
Create a Console Application Project in Visual Studio
First, let's create a .NET Core console application project in Visual Studio. Assuming you have Visual Studio 2019 installed on your system, follow the steps outlined below to create a new .NET Core console application project in Visual Studio.
- Launch the Visual Studio IDE.
- Click "Create new project."
- In the "Create new project" window, select "Console App (.NET Core)" from the list of templates displayed.
- Click Next.
- In the "Configure your new project" window that appears next, specify the name and location for the new project.
- Click Create.
Following these steps will create a new .NET Core console application project in Visual Studio 2019. We will use this project in the subsequent sections of this article.
Using init-only properties in C# 9
init-only properties are those that can only be assigned a value when the object is initialized. See the following class that contains init-only properties.
public class DbMetadata
{
public string DbName { get; init; }
public string DbType { get; init; }
}
You can create an instance of the DbMetadata class and initialize its properties using the following code snippet.
DbMetadata dbMetadata = new DbMetadata()
{
DbName = "Test",
DbType = "Oracle"
};
Note that subsequent assignments to an init-only field are illegal. Therefore, the following statement will not compile.
dbMetadata.DbType = "SQL Server";
Using record types in C# 9
The record type in C# 9 is a lightweight, immutable data type (or lightweight class) with only read-only properties. Because the record type is immutable, it is thread-safe and cannot be changed or altered after creation. You can only initialize a record type in a constructor.
You can declare a record using the record keyword, as shown in the following code snippet.
public record Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
Note that simply marking a type as record (as in the previous code snippet) does not in itself give you immutability. To provide immutability for your record type, you must use init properties, as shown in the following code snippet.
public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public string Address { get; init; }
public string City { get; init; }
public string Country { get; init; }
}
You can create an instance of the Person class and initialize its properties using the following code snippet.
var person = new Person
{
FirstName = "Joydip",
LastName = "Kanjilal",
Address = "192/79 Stafford Hills",
City = "Hyderabad",
Country = "India"
};
Using with expressions in C# 9
You may often want to create an object from another object if some properties have the same values. However, the init-only properties of record types prevent this. For example, the following code snippet will not compile because all properties of a record type named Person are init-only by default.
var newPerson = person;
newPerson.Address = "112 Stafford Hills";
newPerson.City = "Bangalore";
Fortunately, there is a workaround—the with keyword. By specifying the changes in property values, you can leverage the with keyword to create an instance from another record type. The following code snippet illustrates how to achieve this.
var newPerson = person with
{ Address = "112 Stafford Hills", City = "Bangalore" };
Inheritance of record types in C# 9
record types support inheritance. That is, you can create a new record type from an existing record type and add new properties. The following code snippet illustrates how to create a new record type by extending an existing one.
public record Employee : Person
{
public int Id { get; init; }
public double Salary { get; init; }
}
Positional records in C# 9
By default, instances of record types created with positional parameters are immutable. In other words, you can create immutable instances of a record type by passing an ordered list of arguments using constructor parameters, as shown in the code snippet below.
var person = new Person("Joydip", "Kanjilal", "192/79 Stafford Hills", "Hyderabad", "India");
Checking equality of record instances in C# 9
When checking equality of two instances of a class in C#, the comparison is based on the references (identity) of those objects. However, if you check equality of two instances of a record type, the comparison is based on the values in the instances of the record type.
The following code snippet illustrates a record type named DbMetadata consisting of two string properties.
public record DbMetadata
{
public string DbName { get; init; }
public string DbType { get; init; }
}
The following code snippet shows how to create two instances of the DbMetadata record type.
DbMetadata dbMetadata1 = new DbMetadata()
{
DbName = "Test",
DbType = "Oracle"
};
DbMetadata dbMetadata2 = new DbMetadata()
{
DbName = "Test",
DbType = "SQL Server"
};
You can check equality using the Equals method. The following two statements will display "false" in the console window.
Console.WriteLine(dbMetadata1.Equals(dbMetadata2));
Console.WriteLine(dbMetadata2.Equals(dbMetadata1));
Consider the following code snippet that creates a third instance of the DbMetadata record type. Note that instances dbMetadata1 and dbMetadata3 contain the same values.
DbMetadata dbMetadata3 = new DbMetadata()
{
DbName = "Test",
DbType = "Oracle"
};
The following two statements will display "true" in the console window.
Console.WriteLine(dbMetadata1.Equals(dbMetadata3));
Console.WriteLine(dbMetadata3.Equals(dbMetadata1));
Although record types are reference types, C# 9 provides synthesized methods to follow value-based equality semantics. The compiler generates the following methods for your record type to enforce value-based semantics:
- An override of the
Object.Equals(Object)method - A virtual
Equalsmethod that accepts therecordtype as its parameter - An override of the
Object.GetHashCode()method - Methods for the two equality operators, i.e., the
==operator and the!=operator - The
recordtype implementsSystem.IEquatable<T>
Additionally, record types provide an override of the Object.ToString() method. These methods are implicitly generated, and you do not need to reimplement them.
Checking the Equals method in C#
You can check whether the Equals method has been implicitly generated. To do this, add an Equals method in the DbMetadata record as shown below.
public record DbMetadata
{
public string DbName { get; init; }
public string DbType { get; init; }
public override bool Equals(object obj) =>
obj is DbMetadata dbMetadata && Equals(dbMetadata);
}
When you compile the code, the compiler will flag an error with the following message:
Type 'DbMetadata' already defines a member called 'Equals' with the same parameter types
Although a record type is a class, the record keyword provides additional value-type-like behavior and semantics that make record different from a class. record itself is a reference type, but it uses its own built-in equality checking—equality is checked by value rather than by reference. Finally, note that record types can be mutable, but they are primarily designed for immutability.