Introduction
C# 9.0 introduced a powerful new feature: record
. Designed to simplify immutable object creation, records prioritize data over identity, making them ideal for scenarios where value-based equality is essential. In this post, we'll dive into what records are, how to use them, their pros and cons, and practical use cases to help you decide when to use them in your projects.
What Are Records in C#?
A record
is a special kind of class or struct designed to work seamlessly with immutable (read-only) data. Its most compelling feature is nondestructive mutation, allowing you to create modified copies of objects without altering the original. Beyond this, records excel in scenarios where you need types that simply hold or combine data.
In simple use cases, records eliminate boilerplate code while adhering to equality semantics ideal for immutable types. Whether you're working with data transfer objects (DTOs) or configuration models, records offer a clean and concise way to represent data.
Defining a Record
Defining a record is similar to defining a class or struct. Records can include fields, properties, methods, and more. They can implement interfaces and, in the case of class-based records, even inherit from other class-based records.
public record Person(string FirstName, string LastName);
This creates an immutable Person
record with the following features:
-
Primary Constructor: Automatically initializes
FirstName
andLastName
. -
Value-Based Equality: Two
Person
records with the same values are considered equal. -
Auto-Generated
ToString()
: Provides a human-readable string representation.
Features of Records
1. Immutability by Default
Records prioritize immutability, ensuring that data can't be modified after creation. With init
accessors, you can initialize properties only during object creation:
public record Car
{
public string Make { get; init; }
public string Model { get; init; }
}
2. Value-Based Equality
Unlike classes that rely on reference-based equality, records compare property values:
var car1 = new Car { Make = "Tesla", Model = "Model S" };
var car2 = new Car { Make = "Tesla", Model = "Model S" };
Console.WriteLine(car1 == car2); // True
3. Nondestructive Mutation
With the with
expression, you can create a modified copy of an existing record:
var car3 = car1 with { Model = "Model 3" };
Console.WriteLine(car3); // Car { Make = Tesla, Model = Model 3 }
4. Deconstruction
Records support deconstruction for easy extraction of properties:
var (make, model) = car1;
Console.WriteLine($"{make} {model}"); // Tesla Model S
5. Inheritance and Interfaces
Records can implement interfaces and inherit from other records:
public record Employee(string Name, int Id) : Person(Name);
When to Use Records
Use Cases:
- Data Transfer Objects (DTOs): Simplify objects passed between layers.
- Immutable Configuration: Store settings or configurations that shouldn't change.
- Functional Programming: Handle immutable state and transformations effectively.
- Representing Value-Based Data: E.g., geographic coordinates, complex numbers, etc.
Pros and Cons of Records
Pros
- Simplified Syntax: Primary constructors reduce boilerplate code.
- Immutability: Ensures consistent and predictable data handling.
- Value-Based Equality: Automatically handles equality logic for properties.
-
Nondestructive Mutation:
with
expressions make updates cleaner and safer. -
Readability: Auto-generated
ToString()
improves debugging and logging.
Cons
- Limited to C# 9.0+: Requires .NET 5 or later, making it unavailable in older projects.
- Learning Curve: Developers new to immutability might need time to adjust.
- Performance Overhead: Extra work is required for equality checks in large objects.
- Inheritance Limitations: Records only support inheritance within other records, not classes or structs.
- Not Always Necessary: Overhead may not justify use in simple scenarios.
Comparison: Classes vs. Records vs. Structs
Feature | Class | Struct | Record |
---|---|---|---|
Immutability | Optional | Optional | Default |
Equality | Reference-Based | Value-Based | Value-Based |
Inheritance | Full Support | None | Limited to Records |
Performance | Reference Type (Heap) | Value Type (Stack) | Reference Type (Heap) |
Syntax | Verbose | Concise | Concise with Primary Constructor |
Conclusion
Records introduce a new way to define types in C#, complementing the existing class and struct definitions. Here's how and when to use each:
- Class Types: Use class definitions to create object-oriented hierarchies that focus on the responsibilities and behavior of objects. Classes emphasize reference-based equality and are ideal for encapsulating both state and behavior.
- Struct Types: Use struct types for lightweight data structures that primarily store data and are small enough to be copied efficiently. Structs emphasize value-based equality and are best suited for scenarios where copying the entire object is inexpensive.
- Record Types: Use record types when you want value-based equality and comparison, immutability, and the convenience of reference semantics. Records are perfect for scenarios where objects represent data and you don’t want to manually implement equality logic.
- Record Struct Types: Use record struct types when you want the benefits of records—such as value-based equality and immutability—but for types that are small enough to copy efficiently. These are ideal for high-performance scenarios involving value semantics.
Top comments (0)