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 |