If you write C#, you eventually need to decide whether a type should be a class, a struct, or a record. All three can represent data, but they communicate different intent and come with different tradeoffs around equality, mutability, and usage.
I’ll walk you through how these types are commonly used in C#, how they differ, and some practical guidance for choosing among them.
A Useful Distinction: Identity vs. Value
One way to compare these types is by looking at whether a type represents identity or value.
A type with…
If you write C#, you eventually need to decide whether a type should be a class, a struct, or a record. All three can represent data, but they communicate different intent and come with different tradeoffs around equality, mutability, and usage.
I’ll walk you through how these types are commonly used in C#, how they differ, and some practical guidance for choosing among them.
A Useful Distinction: Identity vs. Value
One way to compare these types is by looking at whether a type represents identity or value.
A type with identity represents a specific instance that changes over time. Even if two instances have the same data, they are not interchangeable. A type that represents a value is primarily defined by the data it contains. Two instances with the same data are typically considered equal.
This distinction helps explain why class, record, and struct behave the way they do.
Quick Comparison
| Feature | Class | Record (class) | Struct (record struct) |
|---|---|---|---|
| Type semantics | Reference type | Reference type | Value type |
| Mutability | Mutable by default | Immutable by default | Mutable by default; best practice is to make immutable using the readonly keyword |
| Equality | Reference equality | Value-based equality | Value-based equality |
| Inheritance | Full inheritance | Record-to-record inheritance | No inheritance |
| Common use cases | Domain entities, rich behavior, identity | DTOs, immutable data models, value objects | Small, lightweight values |
Classes
class types are reference types. Assigning or passing a class variable copies a reference, not the object itself. By default, equality is based on reference identity unless explicitly overridden.
Classes are commonly used for:
- domain entities
- types with behavior and invariants
- objects with a clear lifecycle
Here’s a simple example using a cat as a domain entity:
public class Cat
{
public Guid Id { get; }
public string Name { get; private set; }
public int LivesRemaining { get; private set; } = 9;
public Cat(Guid id, string name)
{
Id = id;
Name = name;
}
public void LoseLife()
{
LivesRemaining--;
}
}
In this example, the identity of the Cat matters more than the values of its properties. Even if two cats have the same name and number of lives, they are not interchangeable.
Records
Records were introduced to simplify modeling data-centric types. A record provides value-based equality by default and encourages immutability.
Records are commonly used for:
- Data Transfer Objects (DTOs)
- request and response models
- messages and result types
Here’s an example using a book recommendation, written with explicit properties instead of positional parameters:
public record BookRecommendation
{
public string Title { get; init; }
public string Author { get; init; }
public int YearPublished { get; init; }
}
If two instances of BookRecommendation contain the same data, they’re considered equal. This behavior is often desirable when comparing or passing around data objects.
By default, records are reference types (record class), though record struct is also available when value-type semantics are needed.
Structs
struct types are value types. Assigning or passing a struct copies the entire value. This makes them suitable for small, self-contained data, but it also means their design needs to be deliberate.
Structs are commonly used for:
- small value objects
- types that represent a single concept
Here’s a small example using a book rating:
public readonly struct Rating
{
public int Stars { get; }
public Rating(int stars)
{
if (stars < 1 || stars > 5)
{
throw new ArgumentOutOfRangeException(nameof(stars));
}
Stars = stars;
}
}
Because structs are copied by value, they are typically designed to be immutable. Mutable structs can lead to subtle bugs, especially when used in collections or passed through interfaces.
Common Points of Confusion
Here are a few areas of confusion:
- Records are not always value types;
record classis still a reference type. - Structs are copied on assignment, which affects behavior.
- Changing a type from class to record changes its equality semantics.
record structbehaves like a struct, not a class.
Choosing: Class, Record, or Struct?
While there is no single rule that fits every situation, these guidelines reflect common usage in C#:
- Use
classwhen a type represents an entity with identity or behavior. - Use
recordwhen a type represents data and value-based equality is important. - Use
structwhen modeling a small value where copying is expected.
Understanding how these types behave makes it easier to choose the one that best fits the role the type plays in your code.