Structs and Records
Definition
- Structs are lightweight value types suitable for small data structures that have value semantics. They are allocated on the stack (in most cases) and copied by value.
- Records (C# 9+) are reference or value types that provide value-based equality, immutability-friendly syntax, and non-destructive mutation via
with-expressions.
Core Concepts
Structs
Structs are value types. When you assign a struct, the entire contents are copied.
Primitive Types Are Structs
Many C# primitive types are actually structs under the hood:
| C# Keyword | Actual Type | Category |
|---|---|---|
int | System.Int32 | Struct |
long | System.Int64 | Struct |
float | System.Single | Struct |
double | System.Double | Struct |
decimal | System.Decimal | Struct |
bool | System.Boolean | Struct |
char | System.Char | Struct |
byte | System.Byte | Struct |
string | System.String | Class |
object | System.Object | Class |
This is why these types have methods and properties (e.g., int.MaxValue, "hello".Length).
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
Readonly Structs (C# 7.2)
Marking a struct readonly guarantees the compiler that no member modifies state.
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
public Money Add(Money other) =>
Currency == other.Currency
? new Money(Amount + other.Amount, Currency)
: throw new InvalidOperationException("Currency mismatch");
}
Constructor Field Initialization (Pre-C# 11 vs C# 11+)
In a custom struct constructor (before C# 11), all fields must be explicitly assigned — leaving even one unassigned causes a compile error:
public struct Time
{
private int _hours, _minutes, _seconds;
// Pre-C# 11: must assign ALL fields, or compile error
public Time(int hours, int minutes, int seconds)
{
_hours = hours;
_minutes = minutes;
_seconds = seconds; // omitting this would cause CS0171
}
}
From C# 11, the compiler automatically initializes any field not explicitly assigned to its default value (0, null, etc.) within a constructor. This eliminates the need to manually zero-initialize every field.
Structs always have an implicit parameterless constructor (even if you define other constructors) that zero-initializes all fields. You cannot define your own parameterless constructor before C# 10 — and even in C# 10+, it must initialize all fields.
Value-Type Copy Behavior
Because structs are value types, passing one to a method copies the entire struct. Modifications inside the method do not affect the original:
public struct Counter
{
public int Value { get; set; }
public Counter(int value) => Value = value;
}
var c = new Counter(10);
Console.WriteLine(c.Value); // 10
ChangeValue(c);
Console.WriteLine(c.Value); // Still 10 — struct was copied
static void ChangeValue(Counter counter) => counter.Value = 99;
Use ref or out parameters if you need the method to modify the original struct.
Records
Records combine the best of classes and value semantics — they use value-based equality (two records are equal if all their data is equal).
Record Classes (C# 9+)
// Positional record — concise syntax
public record Person(string FirstName, string LastName, int Age);
// Usage
var p1 = new Person("Alice", "Smith", 30);
var p2 = new Person("Alice", "Smith", 30);
Console.WriteLine(p1 == p2); // True — value-based equality
Console.WriteLine(ReferenceEquals(p1, p2)); // False — different references
With-Expressions (Non-Destructive Mutation)
var original = new Person("Alice", "Smith", 30);
var updated = original with { Age = 31 }; // creates a new copy with one field changed
Deconstruction
var person = new Person("Alice", "Smith", 30);
var (first, last, age) = person; // deconstructs positional parameters
Record Structs (C# 10+)
public record struct Point(double X, double Y);
// Readonly record struct
public readonly record struct Money(decimal Amount, string Currency);
Code Examples
Mutable Struct Pitfall
public struct BadCounter
{
public int Value;
public void Increment() => Value++; // mutates in-place
}
BadCounter c = new BadCounter();
c.Increment();
Console.WriteLine(c.Value); // 1
List<BadCounter> list = new() { new BadCounter() };
list[0].Increment(); // MODIFIES A COPY — Value stays 0!
Console.WriteLine(list[0].Value); // 0 — bug!
Mutable structs cause subtle bugs when used in collections, behind interfaces, or in using blocks. Always design structs as immutable. Use readonly struct to enforce this at compile time.
Record with Additional Members
public record Student(string FirstName, string LastName, double Gpa)
{
public string FullName => $"{FirstName} {LastName}";
public virtual bool Equals(Student? other) =>
other is not null && FirstName == other.FirstName && LastName == other.LastName;
}
Primary Constructors for Classes (C# 12)
C# 12 introduced primary constructors for regular classes. Unlike records, these classes do not get value-based equality or with-expressions.
// Class with primary constructor — NOT a record
public class Product(string name, decimal price)
{
public string Name { get; } = name;
public decimal Price { get; } = price;
public override string ToString() => $"{Name}: {price:C}";
}
var p1 = new Product("Widget", 9.99m);
var p2 = new Product("Widget", 9.99m);
Console.WriteLine(p1 == p2); // False — reference equality, not value-based
A class with a primary constructor is still a regular class — it uses reference equality and has no with-expression support. Use record when you need value-based semantics.
Comparison Table
| Feature | class | struct | record class | record struct |
|---|---|---|---|---|
| Type | Reference | Value | Reference | Value |
| Equality | Reference | Value (bitwise) | Value-based | Value-based |
| Inheritance | Yes (single) | No | Yes (single) | No |
| Can be mutable | Yes | Yes (but avoid) | Yes (but avoid) | Yes (but avoid) |
with expression | No | No | Yes | Yes |
| Primary use case | Complex behavior | Small data (< 16 bytes) | DTOs, value objects | Small value objects |
When to Use Which
Decision Guidance
- Need inheritance and complex behavior? Use a class.
- Small (< 16 bytes), short-lived, no boxing expected? Use a struct (preferably
readonly struct). - Immutable data, value-based equality, DTOs, or DDD value objects? Use a record (
record classorrecord struct). - Small immutable value object that needs
with? Use areadonly record struct.
When to Use a Class
- The type represents an entity with identity (e.g.,
User,Order). - You need inheritance hierarchies.
- The object is large or long-lived.
- Reference semantics are desirable.
When to Use a Struct
- The type is small (Microsoft recommends under 16 bytes).
- It represents a single value (e.g.,
Point,Color,Money). - It is short-lived and commonly allocated in method scope.
- You want to avoid heap allocation and GC pressure.
When to Use a Record
- DTOs, API request/response models.
- Domain value objects (DDD).
- Immutable data that you need to copy with minor changes (
with). - You want structural equality out of the box.
Common Pitfalls
- Boxing structs — casting a struct to an interface or
objectallocates on the heap. In hot paths, this causes GC pressure. - Mutable structs in collections — modifying a struct through a collection indexer modifies a copy, not the original. Always use
readonly struct. - Records are not always immutable — you can add
setaccessors or mutable fields to a record. Immutability is a design choice, not enforced. - Structs cannot inherit from other structs — they can only implement interfaces.
- Default constructor of a struct — always initializes all fields to zero/null. You cannot prevent this.
public struct Temperature
{
public decimal Value { get; } // default(Temperature).Value == 0, even if 0 is invalid
}
Key Takeaways
- Structs are value types — use for small, immutable data where copy semantics are desired.
- Records provide value-based equality and
with-expressions — ideal for DTOs and value objects. - Prefer
readonly structto guarantee struct immutability at compile time. - C# 12 primary constructors for classes do not provide record features (equality,
with). - Keep structs under 16 bytes to avoid performance penalties from copying.
Interview Questions
Q: When would you use a struct over a class?
A: When the type is small (under ~16 bytes), represents a single value, is short-lived, and you want value semantics (copy on assignment). Examples: Point, Color, Guid.
Q: What is a record?
A: A record is a reference type (or value type with record struct) that provides value-based equality, non-destructive mutation via with-expressions, and concise positional syntax for immutable data models.
Q: What is value-based equality? A: Two record instances are considered equal if all their fields and properties have the same values, regardless of whether they are the same object in memory. This differs from classes, which use reference equality by default.
Q: What does the with expression do?
A: It creates a shallow copy of a record with one or more properties changed, leaving the original unchanged. It is a form of non-destructive mutation.
Q: What is boxing and how does it relate to structs?
A: Boxing is the process of converting a value type to a reference type (e.g., casting a struct to object or an interface). This allocates a new object on the heap and copies the struct's data into it. Frequent boxing in hot paths degrades performance through increased GC pressure.