C# Record Structs — Record Struct, Readonly Record Struct, and Positional Syntax
In this tutorial, you will learn about C# Record Structs. We cover key concepts, practical examples, and best practices to help you master this topic.
C# record structs (C# 10+) combine the value-type efficiency of structs with the immutable data features of records, including value-based equality, positional construction, and non-destructive mutation through with expressions.
What You'll Learn
You will master record structs in C#: declaring readonly record struct for immutable value data, positional record structs for concise syntax, with expressions for mutation, value equality for structs, and when to choose record struct over record class or regular struct in .NET.
Why It Matters
Record structs solve a common problem: you want the performance of a value type (stack allocation, no GC pressure) with the convenience of records (value equality, ToString, Deconstruct). Before record structs, you had to manually implement all these features for structs. Record structs make value-type Data Modeling as convenient as class-based records.
Real-World Use
Game engines use record structs for vector math (Vector3, Quaternion). Image processing uses record structs for pixel data. Financial systems use record structs for small value objects (Money, Percentage). Geographic systems use record structs for coordinates. Scientific computing uses record structs for complex numbers and matrices.
Learning Path
graph LR
A["27: Pattern Matching"] --> B["28: Record Structs"]
B --> C["29: Async Await"]
C --> D["30: Parallel Programming"]
D --> E["31: Span Memory"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Readonly Record Struct
public readonly record struct Point(float X, float Y);
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
var p3 = p1 with { Y = 5 };
Console.WriteLine(p1); // Point { X = 3, Y = 4 }
Console.WriteLine(p1 == p2); // True (value equality)
Console.WriteLine(p3); // Point { X = 3, Y = 5 }
Console.WriteLine(p1.GetType().IsValueType); // True
// Deconstruction
var (x, y) = p1;
Console.WriteLine($"X: {x}, Y: {y}"); // X: 3, Y: 4
Record Struct vs Record Class
// Reference type (heap allocated)
public record class PersonClass(string Name, int Age);
// Value type (stack allocated)
public readonly record struct PersonStruct(string Name, int Age);
// Both support:
// - Positional construction
// - Value equality
// - ToString
// - Deconstruct
// - with expressions
// But struct is:
// - Stack allocated (no GC)
// - Passed by value (copied)
// - Cannot be null (unless nullable)
// - Cannot have inheritance
// - Smaller and faster for small data
Adding Methods and Computed Properties
public readonly record struct Vector3(float X, float Y, float Z)
{
public float Magnitude => MathF.Sqrt(X * X + Y * Y + Z * Z);
public Vector3 Normalized()
{
var mag = Magnitude;
return mag > 0 ? this / mag : this;
}
public float Dot(Vector3 other) =>
X * other.X + Y * other.Y + Z * other.Z;
public Vector3 Cross(Vector3 other) => new(
Y * other.Z - Z * other.Y,
Z * other.X - X * other.Z,
X * other.Y - Y * other.X
);
public static Vector3 operator +(Vector3 a, Vector3 b) =>
new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator -(Vector3 a, Vector3 b) =>
new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vector3 operator *(Vector3 v, float s) =>
new(v.X * s, v.Y * s, v.Z * s);
public static Vector3 operator /(Vector3 v, float s) =>
new(v.X / s, v.Y / s, v.Z / s);
}
Non-Readonly Record Struct
Mutable record structs are possible but rarely needed:
public record struct MutablePoint
{
public float X { get; set; }
public float Y { get; set; }
}
var p = new MutablePoint { X = 1, Y = 2 };
p.X = 10; // Mutable
Console.WriteLine(p); // MutablePoint { X = 10, Y = 2 }
Record Struct with Validation
public readonly record struct Temperature
{
public double Celsius { get; }
public double Fahrenheit => Celsius * 9 / 5 + 32;
public Temperature(double celsius)
{
if (celsius < -273.15)
throw new ArgumentException("Temperature below absolute zero");
Celsius = celsius;
}
// Parameterless constructor for default
public Temperature() : this(0) { }
}
Record Structs in Collections
// Record structs work great in arrays (contiguous memory)
var points = new Point[1000];
for (int i = 0; i < points.Length; i++)
points[i] = new Point(i, i * 2);
// Fast iteration due to cache locality
float total = 0;
foreach (var p in points)
total += p.X + p.Y;
// As dictionary keys (value equality works)
var dict = new Dictionary<Point, string>
{
[new Point(0, 0)] = "Origin",
[new Point(1, 1)] = "Unit"
};
Console.WriteLine(dict[new Point(0, 0)]); // Origin
Record Struct vs Class Performance
// Record struct: 16 bytes, stack allocated, no GC
readonly record struct SmallValue(int A, int B, int C, int D);
// Record class: heap allocated, GC tracked
record class SmallClass(int A, int B, int C, int D);
// For arrays:
var structArray = new SmallValue[10000]; // Single allocation, contiguous
var classArray = new SmallClass[10000]; // 10001 allocations, scattered
for (int i = 0; i < 10000; i++)
classArray[i] = new SmallClass(i, i, i, i); // 10000 more allocations
Common Mistakes
Mistake 1: Making Record Structs Too Large
Structs over 16-24 bytes incur copying overhead. For larger data, use record class. Profile before optimizing.
Mistake 2: Using Mutable Record Structs
Mutable structs cause subtle bugs. Always prefer readonly record struct. Mutable structs in collections can produce unexpected behavior.
Mistake 3: Forgetting That Structs Are Passed by Value
Methods receive a copy of the struct. Modifications inside the method do not affect the original. Use in parameter for read-only reference.
Mistake 4: Boxing Record Structs
Assigning a record struct to object or an interface boxes it (heap allocation). This negates the performance benefit. Use generics to avoid boxing.
Mistake 5: Not Implementing IEquatable
Record structs automatically implement IEquatable
Mistake 6: Using Record Struct with Inheritance
Record structs cannot inherit from other structs or classes. They cannot be used as base types. This is by design for value types.
Practice Questions
- What are the key differences between
readonly record structandrecord class? - How does value equality work for record structs?
- When would you choose a record struct over a regular struct?
- What is the performance advantage of record struct arrays over record class arrays?
- Write a
readonly record struct Money(decimal Amount, string Currency)with addition operators and currency conversion.
Challenge
Create a simple ray tracer using record structs for Vector3 (position, direction), Color (R, G, B), and Ray (origin, direction). Implement basic operations and demonstrate value equality for color comparison.
FAQ
Mini Project
Create a 3D math library with record structs:
public readonly record struct Vector4(float X, float Y, float Z, float W)
{
public float Magnitude => MathF.Sqrt(X * X + Y * Y + Z * Z + W * W);
public static Vector4 operator +(Vector4 a, Vector4 b) =>
new(a.X + b.X, a.Y + b.Y, a.Z + b.Z, a.W + b.W);
public static Vector4 operator *(Vector4 v, float s) =>
new(v.X * s, v.Y * s, v.Z * s, v.W * s);
public static float Dot(Vector4 a, Vector4 b) =>
a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W;
public Vector4 Lerp(Vector4 target, float t) =>
this + (target - this) * t;
}
public readonly record struct Matrix4x4(
float M11, float M12, float M13, float M14,
float M21, float M22, float M23, float M24,
float M31, float M32, float M33, float M34,
float M41, float M42, float M43, float M44)
{
public static Matrix4x4 Identity => new(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
public static Vector4 operator *(Matrix4x4 m, Vector4 v) => new(
m.M11 * v.X + m.M12 * v.Y + m.M13 * v.Z + m.M14 * v.W,
m.M21 * v.X + m.M22 * v.Y + m.M23 * v.Z + m.M24 * v.W,
m.M31 * v.X + m.M32 * v.Y + m.M33 * v.Z + m.M34 * v.W,
m.M41 * v.X + m.M42 * v.Y + m.M43 * v.Z + m.M44 * v.W
);
public static Matrix4x4 CreateTranslation(float x, float y, float z) => new(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1
);
}
var v1 = new Vector4(1, 2, 3, 1);
var v2 = new Vector4(4, 5, 6, 1);
Console.WriteLine($"v1: {v1}");
Console.WriteLine($"v1 + v2: {v1 + v2}");
Console.WriteLine($"v1 * 2: {v1 * 2}");
Console.WriteLine($"Dot product: {Vector4.Dot(v1, v2)}");
Console.WriteLine($"v1 magnitude: {v1.Magnitude:F2}");
var lerped = v1.Lerp(v2, 0.5f);
Console.WriteLine($"Lerped (50%): {lerped}");
var translation = Matrix4x4.CreateTranslation(10, 20, 30);
var translated = translation * v1;
Console.WriteLine($"Translated: {translated}");
Expected output:
v1: Vector4 { X = 1, Y = 2, Z = 3, W = 1 }
v1 + v2: Vector4 { X = 5, Y = 7, Z = 9, W = 2 }
v1 * 2: Vector4 { X = 2, Y = 4, Z = 6, W = 2 }
Dot product: 32
v1 magnitude: 3.87
Lerped (50%): Vector4 { X = 2.5, Y = 3.5, Z = 4.5, W = 1 }
Translated: Vector4 { X = 11, Y = 22, Z = 33, W = 1 }
What's Next
You have mastered record structs in C#. The next lesson covers async and await: Task
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro