Skip to main content

C# Record Structs

Introduction

C# 10 introduced a new feature called record structs, combining the benefits of records (introduced in C# 9) with the efficiency of value types. Record structs provide a concise way to define immutable value types with built-in value equality, making them perfect for representing data that shouldn't change after creation.

In this guide, we'll explore what record structs are, how they differ from regular records and structs, and when you should use them in your applications.

What Are Record Structs?

A record struct is a value type that provides:

  1. A concise syntax for creating immutable data models
  2. Built-in value equality (two record structs with the same values are equal)
  3. Value semantics (they're copied when assigned to a new variable)
  4. A convenient ToString() implementation
  5. Built-in deconstruction capabilities

Record structs combine the immutable nature and syntactic convenience of records with the performance advantages of structs for small data objects.

Basic Syntax

Let's start with the basic syntax for declaring a record struct:

csharp
// Basic syntax
public record struct Point(double X, double Y);

This concise declaration creates a value type with two read-only properties (X and Y), along with a constructor, deconstruction method, and equality members.

Record Structs vs Regular Structs

To understand the benefits of record structs, let's compare them with regular structs:

csharp
// Regular struct - requires more code
public struct PointStruct
{
public double X { get; init; }
public double Y { get; init; }

public PointStruct(double x, double y)
{
X = x;
Y = y;
}

public override string ToString()
{
return $"PointStruct {{ X = {X}, Y = {Y} }}";
}

// Would need to override Equals, GetHashCode, etc. for proper equality
}

// Record struct - concise and complete
public record struct PointRecord(double X, double Y);

Let's see how these types behave:

csharp
// Using the types
var point1 = new PointStruct(3, 4);
var point2 = new PointStruct(3, 4);
var record1 = new PointRecord(3, 4);
var record2 = new PointRecord(3, 4);

Console.WriteLine($"Regular structs equal: {point1.Equals(point2)}"); // True
Console.WriteLine($"Record structs equal: {record1 == record2}"); // True
Console.WriteLine($"Regular struct: {point1}"); // PointStruct { X = 3, Y = 4 }
Console.WriteLine($"Record struct: {record1}"); // PointRecord { X = 3, Y = 4 }

Output:

Regular structs equal: True
Record structs equal: True
Regular struct: PointStruct { X = 3, Y = 4 }
Record struct: PointRecord { X = 3, Y = 4 }

Immutability and With-expressions

Record structs are designed to be immutable by default, but you can create a mutable record struct using the readonly keyword. Let's see both options:

csharp
// Immutable record struct (default)
public record struct Temperature(double Celsius);

// Explicitly mutable record struct
public record struct MutablePoint(double X, double Y)
{
public double X { get; set; } = X;
public double Y { get; set; } = Y;
}

// Explicitly readonly record struct
public readonly record struct ReadOnlyPoint(double X, double Y);

When working with immutable record structs, you can use with expressions to create new instances with modified values:

csharp
var temp1 = new Temperature(25.0);
// Create a new record struct with a different value
var temp2 = temp1 with { Celsius = 30.0 };

Console.WriteLine($"Original temperature: {temp1.Celsius}°C");
Console.WriteLine($"New temperature: {temp2.Celsius}°C");

Output:

Original temperature: 25°C
New temperature: 30°C

Value Semantics and Memory Efficiency

Record structs are value types, which means they're stored on the stack (for local variables) rather than the heap. This can lead to better performance for small data objects due to reduced garbage collection pressure.

csharp
public record struct Size(int Width, int Height);

public class Example
{
public static void Main()
{
Size size1 = new Size(10, 20);
Size size2 = size1; // Creates a copy of size1

// This doesn't affect size1
size2 = size2 with { Width = 30 };

Console.WriteLine($"size1: {size1}"); // Size { Width = 10, Height = 20 }
Console.WriteLine($"size2: {size2}"); // Size { Width = 30, Height = 20 }
}
}

Output:

size1: Size { Width = 10, Height = 20 }
size2: Size { Width = 30, Height = 20 }

Practical Use Cases

1. Representing Coordinates

Record structs are perfect for geometric data like points, sizes, or vectors:

csharp
public readonly record struct Vector2D(double X, double Y)
{
public double Length => Math.Sqrt(X * X + Y * Y);

public Vector2D Add(Vector2D other) => new Vector2D(X + other.X, Y + other.Y);

public static Vector2D operator +(Vector2D a, Vector2D b) => a.Add(b);
}

// Usage
Vector2D v1 = new(3, 4);
Vector2D v2 = new(1, 2);
Vector2D sum = v1 + v2;

Console.WriteLine($"v1: {v1}, Length: {v1.Length}");
Console.WriteLine($"v2: {v2}, Length: {v2.Length}");
Console.WriteLine($"Sum: {sum}, Length: {sum.Length}");

Output:

v1: Vector2D { X = 3, Y = 4 }, Length: 5
v2: Vector2D { X = 1, Y = 2 }, Length: 2.2360679774997898
Sum: Vector2D { X = 4, Y = 6 }, Length: 7.211102550927978

2. Configuration Settings

Record structs can represent configuration settings or parameters:

csharp
public readonly record struct ConnectionSettings(
string Host,
int Port,
bool UseSSL,
TimeSpan Timeout
)
{
// Default values
public static ConnectionSettings Default => new(
Host: "localhost",
Port: 8080,
UseSSL: false,
Timeout: TimeSpan.FromSeconds(30)
);
}

// Usage
var defaultSettings = ConnectionSettings.Default;
var productionSettings = defaultSettings with {
Host = "api.example.com",
Port = 443,
UseSSL = true
};

Console.WriteLine($"Default settings: {defaultSettings}");
Console.WriteLine($"Production settings: {productionSettings}");

Output:

Default settings: ConnectionSettings { Host = localhost, Port = 8080, UseSSL = False, Timeout = 00:00:30 }
Production settings: ConnectionSettings { Host = api.example.com, Port = 443, UseSSL = True, Timeout = 00:00:30 }

3. Domain-Specific Types

Record structs can represent domain-specific values with validation:

csharp
public readonly record struct Money(decimal Amount, string Currency)
{
public Money(decimal amount, string currency) : this()
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative", nameof(amount));

if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency cannot be empty", nameof(currency));

Amount = amount;
Currency = currency.ToUpperInvariant();
}

public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException($"Cannot add {other.Currency} to {Currency}");

return this with { Amount = Amount + other.Amount };
}

public override string ToString() => $"{Amount:F2} {Currency}";
}

// Usage
try
{
var price1 = new Money(10.99m, "USD");
var price2 = new Money(5.99m, "USD");
var price3 = new Money(7.99m, "EUR");

var total = price1.Add(price2);
Console.WriteLine($"Price 1: {price1}");
Console.WriteLine($"Price 2: {price2}");
Console.WriteLine($"Total: {total}");

// This will throw an exception
var invalidTotal = price1.Add(price3);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

Output:

Price 1: 10.99 USD
Price 2: 5.99 USD
Total: 16.98 USD
Error: Cannot add EUR to USD

When to Use Record Structs

Record structs are best suited for:

  1. Small data objects (generally less than 16 bytes) - for efficiency
  2. Immutable data that represents values rather than entities
  3. Value equality semantics where two objects with the same values should be considered equal
  4. Data transfer objects or parameter groups that are passed frequently

Use record classes instead when:

  • Your type needs to be larger (to avoid large value copies)
  • You need reference equality instead of value equality
  • You need inheritance capabilities

Advanced Features: Inheritance and Interfaces

While record structs cannot participate in inheritance hierarchies (since they're value types), they can implement interfaces:

csharp
public interface IGeometricShape
{
double Area { get; }
double Perimeter { get; }
}

public readonly record struct Rectangle(double Width, double Height) : IGeometricShape
{
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}

public readonly record struct Circle(double Radius) : IGeometricShape
{
public double Area => Math.PI * Radius * Radius;
public double Perimeter => 2 * Math.PI * Radius;
}

// Usage
IGeometricShape[] shapes = new IGeometricShape[]
{
new Rectangle(10, 5),
new Circle(4)
};

foreach (var shape in shapes)
{
Console.WriteLine($"Shape: {shape}");
Console.WriteLine($"Area: {shape.Area}");
Console.WriteLine($"Perimeter: {shape.Perimeter}");
Console.WriteLine();
}

Output:

Shape: Rectangle { Width = 10, Height = 5 }
Area: 50
Perimeter: 30

Shape: Circle { Radius = 4 }
Area: 50.26548245743669
Perimeter: 25.132741228718345

Summary

C# record structs offer a powerful way to define immutable value types with built-in equality and formatting capabilities. They're excellent for representing small data objects where value semantics make more sense than reference semantics.

Key takeaways:

  • Record structs are value types (stored on the stack)
  • They provide concise syntax for defining immutable data
  • They automatically include value equality, ToString(), and deconstruction capabilities
  • The with expression allows creating new instances with modified values
  • Record structs can implement interfaces but cannot participate in inheritance

When designing your C# applications, consider using record structs for small, immutable data objects that represent values in your domain model.

Exercises

  1. Create a TimeOfDay record struct that represents hours, minutes, and seconds, with validation to ensure the values are within valid ranges.
  2. Implement a DateRange record struct with Start and End dates, and methods to check if a date is within the range or if two ranges overlap.
  3. Define a ColorRGB record struct that represents a color with red, green, and blue values (0-255), with methods to blend colors and convert to hexadecimal format.
  4. Create a system for representing fractions as record structs, with operations for addition, subtraction, multiplication, and division.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)