C# tutorials > Core C# Fundamentals > Data Structures and Collections > What are records in C# 9.0 and later?

What are records in C# 9.0 and later?

Records in C# 9.0 and later provide a concise syntax for creating immutable data types. They are reference types that offer value-based equality, easy deconstruction, and a `ToString` method that displays property names and values. Records are particularly useful for data transfer objects (DTOs) and representing immutable state.

Basic Record Definition

This code defines a simple record named `Person` with two properties: `FirstName` and `LastName`. The properties are defined within the record's constructor, making them immutable after creation. This is a positional record definition which also creates a constructor.

public record Person(string FirstName, string LastName);

Creating and Using a Record

This code shows how to create an instance of the `Person` record and access its properties. Because the properties are defined as part of the primary constructor, the `new` keyword is used along with the provided values.

Person person1 = new Person("John", "Doe");
Console.WriteLine(person1.FirstName); // Output: John
Console.WriteLine(person1.LastName);  // Output: Doe

Value-Based Equality

Records automatically implement value-based equality. This means that two record instances are considered equal if their properties have the same values, regardless of whether they are different objects in memory. This is a significant advantage over classes, where you would typically need to override the `Equals` method to achieve this behavior.

Person person1 = new Person("John", "Doe");
Person person2 = new Person("John", "Doe");

Console.WriteLine(person1 == person2); // Output: True

Deconstruction

Records automatically support deconstruction, allowing you to easily extract the values of their properties into separate variables. The syntax is similar to tuple deconstruction. The parentheses after `var` indicate that the values are being assigned to individual variables.

Person person1 = new Person("John", "Doe");
var (firstName, lastName) = person1;

Console.WriteLine(firstName); // Output: John
Console.WriteLine(lastName);  // Output: Doe

ToString() Override

Records override the `ToString()` method to provide a user-friendly string representation of the record's properties and their values. This is helpful for debugging and logging.

Person person1 = new Person("John", "Doe");
Console.WriteLine(person1); // Output: Person { FirstName = John, LastName = Doe }

Record Inheritance

Records can inherit from other records (or classes). This example shows a `Student` record inheriting from the `Person` record. The Student record defines a new `Major` property and passes the `FirstName` and `LastName` to the base class constructor.

public record Student(string FirstName, string LastName, string Major) : Person(FirstName, LastName);

Student student1 = new Student("Jane", "Smith", "Computer Science");
Console.WriteLine(student1); // Output: Student { FirstName = Jane, LastName = Smith, Major = Computer Science }

Record with Properties (Non-Positional)

Records can also be defined with traditional properties instead of using positional syntax. The `init` accessor ensures that properties can only be set during object initialization, maintaining immutability after creation. This is useful for scenarios where you want more control over the property definitions or need to perform custom validation logic during initialization.

public record Employee
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Using `with` expression for non-destructive mutation

The `with` expression allows you to create a new record instance based on an existing one, but with some properties modified. This is a form of non-destructive mutation, as the original record remains unchanged. In this example, `person2` is a new `Person` record that is identical to `person1`, except for the `City` in the nested `Address` record. Note the chained `with` expressions to modify the inner object. Using `with` for immutable objects allows you to have changes without modifying the source object.

public record Address(string Street, string City, string ZipCode);

public record Person(string FirstName, string LastName, Address Address);

Person person1 = new Person("John", "Doe", new Address("123 Main St", "Anytown", "12345"));

Person person2 = person1 with { Address = person1.Address with { City = "Newville" } };

Console.WriteLine(person1.Address.City); // Output: Anytown
Console.WriteLine(person2.Address.City); // Output: Newville

Real-Life Use Case: Data Transfer Objects (DTOs)

Records are ideal for creating DTOs, which are used to transfer data between different layers of an application. Their immutability ensures that the data remains consistent throughout the process. For example, a DTO representing customer data can be easily created using a record, promoting a cleaner and more robust architecture.

Best Practices

  • Favor Immutability: Design your records to be immutable whenever possible. This promotes data integrity and simplifies reasoning about your code.
  • Use Positional Syntax: When appropriate, use the concise positional syntax for defining records.
  • Consider `with` for Updates: Use the with expression to create updated versions of records without modifying the original instances.

Interview Tip

Be prepared to explain the difference between records and classes in C#. Highlight the value-based equality, immutability, and concise syntax offered by records. Also, understand how the with expression enables non-destructive mutation.

When to Use Them

Use records when you need:

  • Immutable data types.
  • Value-based equality.
  • Concise syntax for defining data structures.
  • Easy deconstruction.
  • A meaningful ToString() implementation.

Memory Footprint

The memory footprint of a record is generally similar to that of a class with the same properties. Records are reference types, so they are stored on the heap. The key difference is that records often encourage immutability, which can lead to more memory usage due to the creation of new instances when data changes. However, this can be offset by improved data integrity and reduced risk of unintended side effects.

Alternatives

Alternatives to records include:

  • Classes: Classes provide more flexibility but require manual implementation of value-based equality and immutability.
  • Structs: Structs are value types that can be used for simple data structures, but they do not support inheritance or default value-based equality.

Pros

  • Concise syntax.
  • Automatic value-based equality.
  • Built-in deconstruction support.
  • Immutable by default (when used correctly).
  • Improved ToString() implementation.

Cons

  • Less flexible than classes for complex scenarios with mutable state.
  • Potential for increased memory usage due to immutability and the creation of new instances.

FAQ

  • Are records reference types or value types?

    Records are reference types, similar to classes. They are stored on the heap, and variables of record types hold references to the objects in memory.
  • Can I have mutable properties in a record?

    Yes, you can have mutable properties in a record, but it's generally recommended to make them immutable using the `init` accessor or by making the record completely immutable. Immutable records are simpler to reason about and less prone to errors.
  • How does the `with` expression work?

    The `with` expression creates a new record instance with the same values as the original, except for the specified properties, which are updated with new values. It performs a shallow copy of the object, so any reference type properties will still point to the same objects in memory unless explicitly changed in the `with` expression.
  • Can I use records in older versions of C#?

    No, records were introduced in C# 9.0. To use records, you need to use a compiler that supports C# 9.0 or later. You might be able to simulate some of the behavior with custom classes, but they will require more code to achieve the same functionality.