C# Record Types

Hi there, welcome to my blog! Today I’m going to talk about a new feature in C# 10 that makes working with data easier and more concise: record types. Record types are a special kind of class or struct that provide built-in functionality for encapsulating data. They are ideal for scenarios where you need to store immutable data, compare objects by value, or create copies with modified values. Let’s see how they work and why they are useful.

What are record types?

Record types are types that use value-based equality. That means two variables of a record type are equal if the record type definitions are identical, and if for every field, the values in both records are equal. For example, consider the following record type that represents a person:

public record Person(string FirstName, string LastName);

This is a positional record type, which means the compiler generates public properties for the primary constructor parameters. You can create an instance of this record type like this:

var alice = new Person("Alice", "Smith");

And you can access the properties like this:

Console.WriteLine(alice.FirstName); // Alice
Console.WriteLine(alice.LastName); // Smith

Now, if you create another instance of the same record type with the same values, they will be considered equal:

var bob = new Person("Alice", "Smith");
Console.WriteLine(alice == bob); // true

This is different from regular classes, where two instances with the same values are not equal unless they refer to the same object in memory. For example, consider the following class that represents a person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

If you create two instances of this class with the same values, they will not be equal:

var alice = new Person("Alice", "Smith");
var bob = new Person("Alice", "Smith");
Console.WriteLine(alice == bob); // false

To make them equal, you would have to override the Equals and GetHashCode methods, and implement the IEquatable<T> interface. This can be tedious and error-prone, especially if you have many properties or fields. Record types save you from writing all that boilerplate code, and let you focus on the data itself.

What else can record types do?

Record types also provide some other features that make working with data easier and more concise. Here are some of them:

  • Nondestructive mutation: Record types support with expressions, which let you create a copy of a record with some values changed. For example, you can create a copy of alice with a different last name like this:
var alice2 = alice with { LastName = "Jones" };
Console.WriteLine(alice2.LastName); // Jones

This does not modify the original record, but creates a new one with the specified changes. This is useful for scenarios where you need to preserve the original data, but also create variations of it.

  • Deconstruction: Record types support deconstruction, which lets you extract the values of the properties or fields into separate variables. For example, you can deconstruct alice like this:
var (firstName, lastName) = alice;
Console.WriteLine(firstName); // Alice
Console.WriteLine(lastName); // Smith

This is useful for scenarios where you need to pass the values of a record to another method, or assign them to other variables.

  • Formatting: Record types provide an override of ToString, which returns a string representation of the record and its values. For example, you can print alice like this:
Console.WriteLine(alice); // Person { FirstName = Alice, LastName = Smith }

This is useful for debugging or logging purposes, or for displaying the data to the user.

  • Inheritance: Record types support inheritance, which lets you create hierarchies of records that share some common properties or fields. For example, you can create a record type that represents a student, which inherits from the person record type:
public record Student(string FirstName, string LastName, int Grade) : Person(FirstName, LastName);

This record type inherits the FirstName and LastName properties from the person record type, and adds a new property Grade. You can create an instance of this record type like this:

var alice = new Student("Alice", "Smith", 10);

And you can access the inherited properties like this:

Console.WriteLine(alice.FirstName); // Alice
Console.WriteLine(alice.LastName); // Smith

You can also use the is operator to check if a record is an instance of a specific record type:

Console.WriteLine(alice is Person); // true
Console.WriteLine(alice is Student); // true

This is useful for scenarios where you need to model different kinds of data that share some common attributes, or where you need to perform different actions based on the type of the record.

How to use record types?

Record types are a new feature in C# 10, which means you need to use a compiler that supports this version of the language. You can use Visual Studio 2022 or the .NET 6 SDK to work with record types.

To use record types, you need to declare them with the record keyword, either as a class or a struct. You can also omit the class keyword to create a record class. You can use positional parameters to define the properties of the record, or use standard property syntax. You can also use with expressions, deconstruction, inheritance, and other features of record types as shown in the previous examples.

Why use record types?

Record types are useful for scenarios where you need to work with data that is immutable, or where you need to compare objects by value, or where you need to create copies with modified values. Record types simplify the process of creating and using such data types, by providing built-in functionality and concise syntax. Record types can help you write cleaner, more expressive, and more maintainable code.

Key Features of C# Records

Records offer several advantages over traditional classes and structures:

  • Compact Syntax: Records use a concise and readable syntax, making it easy to define and understand data structures.
  • Immutable by Default: Records are immutable by default, meaning their properties cannot be changed after creation. This promotes data integrity and makes it easier to reason about code.
  • Automatic Properties: Records automatically generate properties for each member variable, making it simpler to access and manipulate data.
  • Consistent Equality: Records use value-based equality, comparing the actual values of their members rather than their references. This ensures consistent behavior and avoids unexpected object identity issues.
  • Readonly Records: Records can be declared as readonly, making their properties read-only and preventing accidental modifications.

Conclusion

Here is a quick summary of C# Record Type:

  • A record is a special kind of class that has public properties that cannot be changed after creation. Records are immutable, which means they stay the same throughout their lifetime.
  • When you print a record to the console, you get a nice output that shows the record type and its property values. Records have a built-in ToString method that does this for you automatically.
  • If you have a list of records, you can use Linq to filter out the duplicates based on their property values. Records use value-based equality, which means two records are equal if they have the same type and the same values. This is different from classes, which use reference-based equality, which means two classes are equal only if they refer to the same object in memory.
  • Records also support nondestructive mutation, which means you can create a copy of a record with some values changed, without affecting the original record. You can use the with keyword to do this, which creates a new record with the specified changes.
  • Records are not suitable for scenarios where you need to inject dependencies or services into your objects. Records are meant to be simple data containers, not complex business logic holders. If you need to use dependency injection or service locator patterns, you should use classes instead.
  • Records have value-type semantics, which means they behave like primitive types such as int or bool. You can pass records by value, assign them to variables, or use them as return types. Records are lightweight and efficient and can help you avoid unnecessary heap allocations and garbage collection.
  • Records can be used to implement value objects in domain-driven design (DDD). Value objects are objects that represent a specific value or concept in the domain, such as money, email, or address. Value objects are immutable, equal by value, and have no identity. Records match these characteristics perfectly and can help you model your domain more clearly and consistently.

In this blog post, I introduced you to record types, a new feature in C# 10 that makes working with data easier and more concise. I showed you how to declare and use record types, and what features they provide. I hope you found this post informative and helpful, and that you will give record types a try in your next C# project. Thanks for reading, and happy coding!

Leave a Comment