C# Records vs Regular Classes: Key Differences
This article explores C# record classes and how they fundamentally differ from traditional classes. Introduced in C# 9, records provide a concise, data-centric alternative to standard object-oriented classes. You will learn about the core mechanics of records—including value-based equality, non-destructive mutation, and built-in formatting—along with clear guidelines on when to choose a record over a standard class in your applications.
What Is a Record Class?
A record class (defined using the record or
record class keyword) is a reference type designed
specifically for encapsulating data rather than mutable behavior. While
regular classes focus on object identity and state changes, records
prioritize immutability and data consistency, significantly cutting down
boilerplate code for common data structures.
Key Differences Between Records and Classes
1. Equality Semantics: Value Equality vs. Reference Equality
The most significant distinction between records and regular classes is how equality is evaluated:
- Regular Classes: Use reference equality by default. Two distinct instances of a class are considered equal only if they point to the exact same memory address, regardless of whether their property values are identical.
- Records: Use value equality by
default. The compiler synthesizes an
Equalsmethod and overrides the==and!=operators so that two separate record instances are considered equal if they are of the same type and hold identical values for all properties.
// Regular Class
public class ClassPerson
{
public string Name { get; set; }
}
var c1 = new ClassPerson { Name = "Alice" };
var c2 = new ClassPerson { Name = "Alice" };
Console.WriteLine(c1 == c2); // False (different references)
// Record Class
public record RecordPerson(string Name);
var r1 = new RecordPerson("Alice");
var r2 = new RecordPerson("Alice");
Console.WriteLine(r1 == r2); // True (same values)2. Concise Syntax and Immutability
Records support positional syntax, allowing you to declare properties, a primary constructor, and deconstruction logic in a single line.
- Regular Classes: Require explicit constructors, backing fields, and property declarations.
- Records: Properties declared in a positional record
are immutable by default with
init-only accessors.
// Record defined in one line
public record Book(string Title, string Author, decimal Price);This single line automatically generates:
- Public
init-only properties forTitle,Author, andPrice. - A primary constructor to populate them.
- Built-in deconstructors for tuple-like extraction.
3. Non-Destructive
Mutation (with Expression)
Because records are commonly immutable, modifying an object usually
requires creating a new copy with updated values. Records natively
support non-destructive mutation via the with
expression:
var original = new Book("C# in Depth", "Jon Skeet", 39.99m);
var discounted = original with { Price = 29.99m };In a standard class, achieving this requires implementing a custom copy constructor or a cloning method.
4. Built-in
ToString() Formatting
- Regular Classes: By default, calling
ToString()prints the fully qualified type name (e.g.,MyApp.Models.ClassPerson). - Records: The compiler automatically overrides
ToString()to display the record's type name and a formatted list of all its property names and values:
Console.WriteLine(original);
// Output: Book { Title = C# in Depth, Author = Jon Skeet, Price = 39.99 }Summary of Differences
| Feature | Regular Class | Record Class |
|---|---|---|
| Default Equality | Reference (memory address) | Value (property contents) |
| Primary Focus | Encapsulated state and behavior | Immutable data structures |
| Default Mutability | Mutable (get; set;) |
Immutable (get; init;) with
positional syntax |
with
Expression |
Not supported | Supported natively |
Generated
ToString() |
Displays class namespace/name | Displays properties and values |
| Deconstruction | Requires manual Deconstruct
method |
Built-in for positional records |
When to Use Which
- Choose Records for data transfer objects (DTOs), API request and response payloads, event messages, domain values, or any scenario where the identity of the object depends purely on the data it carries.
- Choose Regular Classes for rich domain models with complex internal state, operations that require mutable behavior, or entities where object identity is distinct from object state (such as entities tracked by an ORM like Entity Framework).