Skip to content

C# Record Structs — Record Struct, Readonly Record Struct, and Positional Syntax

DodaTech Updated 2026-06-28 8 min read

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, but if you add mutable reference-type fields, equality may behave unexpectedly.

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

  1. What are the key differences between readonly record struct and record class?
  2. How does value equality work for record structs?
  3. When would you choose a record struct over a regular struct?
  4. What is the performance advantage of record struct arrays over record class arrays?
  5. 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

Can a record struct implement interfaces?

Yes. Record structs can implement interfaces just like regular structs. However, assigning to an interface variable causes boxing.

Do record structs support inheritance?

No. Structs cannot inherit from other structs or classes. Record structs also cannot be base types. This is a fundamental limitation of value types.

How is ToString generated for record structs?

The compiler generates a ToString that shows all public properties with their names and values, similar to record classes. You can override it for custom formatting.

Can I use record structs in async methods?

Yes. Record structs work in async methods like any value type. They are captured by value in closures and async state machines.

What happens if a record struct has a reference-type field?

The struct itself is a value type, but its reference-type field points to heap-allocated data. Equality compares the reference, not the content. Use caution.

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, async/await, ConfigureAwait, and ValueTask.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro