Skip to content

C# Structs — Value Types, Readonly Struct, Ref Struct, and Record Struct

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C# Structs. We cover key concepts, practical examples, and best practices to help you master this topic.

C# structs are value types that provide stack-based allocation and value semantics, with readonly structs for immutability, ref structs for stack-only constraints, and record structs combining value semantics with record features.

What You'll Learn

You will master structs in C#: the difference between structs and classes, when to use value types for performance, readonly structs for immutability, ref structs for stack-only allocation, record structs for immutable value data, and best practices for struct design in .NET.

Why It Matters

Structs are essential for performance-critical code. They avoid heap allocation and Garbage Collection pressure. The .NET runtime and core libraries use structs extensively: int, double, DateTime, TimeSpan, Guid, decimal, and Span<T> are all structs. Choosing structs over classes in the right scenarios can dramatically reduce memory allocation and improve cache locality.

Real-World Use

Game engines use structs for Vector3, Quaternion, and Matrix4x4 to avoid heap allocation. High-frequency trading systems use structs for order book entries. Span and ReadOnlySpan are ref structs enabling zero-allocation slicing. ASP.NET Core uses structs internally for request/response pipelines. Image processing uses structs for pixel data.

Learning Path

graph LR
    A["15: Records"] --> B["16: Structs"]
    B --> C["17: Strings"]
    C --> D["18: Arrays & Collections"]
    D --> E["19: Generics"]
    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

Basic Struct

public struct Point
{
    public double X;
    public double Y;

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

    public double DistanceTo(Point other)
    {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

var p1 = new Point(3, 4);
var p2 = new Point(0, 0);
Console.WriteLine($"Distance: {p1.DistanceTo(p2):F2}");  // 5.00

Struct vs Class Memory Behavior

public struct PointStruct
{
    public int X;
    public int Y;
}

public class PointClass
{
    public int X;
    public int Y;
}

// Struct: copy semantics
var ps1 = new PointStruct { X = 10, Y = 20 };
var ps2 = ps1;  // Creates a copy
ps2.X = 99;
Console.WriteLine($"ps1: ({ps1.X}, {ps1.Y})");  // (10, 20)

// Class: reference semantics
var pc1 = new PointClass { X = 10, Y = 20 };
var pc2 = pc1;  // Same object
pc2.X = 99;
Console.WriteLine($"pc1: ({pc1.X}, {pc1.Y})");  // (99, 20)

Readonly Struct

Prevents modification of fields after construction:

public readonly struct Vector3
{
    public readonly float X;
    public readonly float Y;
    public readonly float Z;

    public Vector3(float x, float y, float z)
    {
        X = x;
        Y = y;
        Z = z;
    }

    // Methods must be readonly too
    public readonly float Magnitude() =>
        MathF.Sqrt(X * X + Y * Y + Z * Z);

    public readonly Vector3 Normalized()
    {
        var mag = Magnitude();
        return mag > 0 ? new(X / mag, Y / mag, Z / mag) : this;
    }
}

var v = new Vector3(3, 4, 0);
Console.WriteLine($"Magnitude: {v.Magnitude()}");  // 5

Ref Struct

Ref structs are stack-only: they cannot be boxed, assigned to object, or used as a field of a class:

public ref struct SpanWrapper
{
    private readonly Span<byte> _data;

    public SpanWrapper(Span<byte> data)
    {
        _data = data;
    }

    public byte this[int index]
    {
        get => _data[index];
        set => _data[index] = value;
    }

    public int Length => _data.Length;
}

// Usage
Span<byte> buffer = stackalloc byte[256];
var wrapper = new SpanWrapper(buffer);
wrapper[0] = 42;
Console.WriteLine(wrapper[0]);  // 42

Record Struct (C# 10)

Combines value-type semantics with record features:

public readonly record struct Color(byte R, byte G, byte B, byte A = 255);

// Usage
var red = new Color(255, 0, 0);
var halfOpacity = red with { A = 128 };

Console.WriteLine(red);            // Color { R = 255, G = 0, B = 0, A = 255 }
Console.WriteLine(halfOpacity);    // Color { R = 255, G = 0, B = 0, A = 128 }

// Value equality
var red2 = new Color(255, 0, 0);
Console.WriteLine(red == red2);  // True

When to Use Struct

Microsoft recommends using struct when:

  • The type represents a single value (like Point, Color, Complex)
  • The instance size is 16 bytes or less (fewer than 8 bytes ideal)
  • The type is immutable
  • The type will not be boxed frequently
  • Short-lived instances in arrays are common
// Good candidate: small, immutable, value-like
public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return new Money(a.Amount + b.Amount, a.Currency);
    }
}

Struct Constructors and Initialization

public struct Config
{
    public int Timeout;
    public string? ServerUrl;
    public bool IsEnabled;

    // Parameterized constructor
    public Config(int timeout, string? serverUrl, bool isEnabled)
    {
        Timeout = timeout;
        ServerUrl = serverUrl;
        IsEnabled = isEnabled;
    }
}

// Structs have an implicit parameterless constructor that zero-initializes
var defaultConfig = default(Config);
Console.WriteLine(defaultConfig.Timeout);  // 0
Console.WriteLine(defaultConfig.ServerUrl); // (null)

// Usage
var config = new Config(30, "https://api.example.com", true);

Performance: Struct Array vs Class Array

public struct SmallStruct { public int X; public int Y; }
public class SmallClass { public int X; public int Y; }

// Struct array: contiguous memory, single allocation
var structs = new SmallStruct[1000];

// Class array: 1000 separate heap objects + array allocation
var classes = new SmallClass[1000];
for (int i = 0; i < 1000; i++) classes[i] = new SmallClass();

Struct arrays are significantly faster for iteration due to cache locality and reduced indirection.

Struct Limitations

public struct LimitedStruct
{
    // Cannot have:
    // - Parameterless constructor with field initializers (C# 10 allows default values)
    // - Virtual or abstract members
    // - Finalizer (destructor)
    // - Inheritance (structs cannot inherit from other structs)
    // - Default constructor without field initialization (before C# 10)

    public int Id { get; set; }
}

Common Mistakes

Mistake 1: Making Structs Too Large

Structs over 16-24 bytes should generally be classes. Large structs cause excessive copying when passed to methods or stored in collections.

Mistake 2: Mutable Structs (Anti-Pattern)

Mutable structs cause subtle bugs. When stored in collections or passed as parameters, modifications apply to copies. Always prefer readonly struct with readonly fields.

Mistake 3: Boxing Structs Frequently

Passing structs to methods expecting object or IEnumerable causes boxing (heap allocation). Use generics to avoid this.

Mistake 4: Using Ref Struct As a Generic Parameter

Ref structs cannot be used as type arguments: List<SpanWrapper> does not compile. This is by design to prevent them from escaping the stack.

Mistake 5: Forgetting That Default Construction Zero-Initializes

new MyStruct() or default(MyStruct) zero-initializes all fields. Reference type fields become null. Accessing them without checking causes null reference exceptions.

Mistake 6: Assuming Struct Methods Cannot Modify this

In non-readonly structs, methods can modify fields. In readonly structs, methods must be marked readonly or they cannot modify state.

Practice Questions

  1. What are the main differences between a struct and a class?
  2. When would you use a readonly struct over a regular struct?
  3. What is the purpose of ref struct? What are its limitations?
  4. Why should structs generally be small (under 16 bytes)?
  5. Write a ComplexNumber struct with value semantics and arithmetic operators.

Challenge

Create a readonly record struct Matrix2x2 with 4 float fields and methods for determinant, inverse, and multiplication. Compare its memory usage and performance with a class equivalent using BenchmarkDotNet (hypothetical).

FAQ

Can structs implement interfaces?

Yes. Structs can implement interfaces. However, when a struct is assigned to an interface variable, it is boxed (heap allocated). This may negate the performance benefit of using a struct.

Can structs have events?

Structs can declare events but rarely should. Events require reference semantics to make sense. If you need events, use a class.

What happens when I pass a struct to a method?

The struct is copied (passed by value). Unless you use the ref, in, or out modifier, the method operates on a copy. Modifications inside the method do not affect the original.

Can a struct have a parameterless constructor?

In C# 10+, structs can have explicit parameterless constructors. Before C# 10, only parameterized constructors were allowed. The implicit parameterless constructor always exists and zero-initializes.

What is the difference between `readonly struct` and `readonly` members?

A readonly struct enforces that all fields are readonly. Individual readonly members on a non-readonly struct only prevent that specific method from modifying state.

Mini Project

Create a 2D math library using structs:

public readonly struct Vector2
{
    public readonly float X;
    public readonly float Y;

    public Vector2(float x, float y) => (X, Y) = (x, y);

    public float Magnitude => MathF.Sqrt(X * X + Y * Y);
    public Vector2 Normalized => Magnitude > 0 ? this / Magnitude : this;

    public static Vector2 operator +(Vector2 a, Vector2 b) => new(a.X + b.X, a.Y + b.Y);
    public static Vector2 operator -(Vector2 a, Vector2 b) => new(a.X - b.X, a.Y - b.Y);
    public static Vector2 operator *(Vector2 v, float s) => new(v.X * s, v.Y * s);
    public static Vector2 operator /(Vector2 v, float s) => new(v.X / s, v.Y / s);
    public static float Dot(Vector2 a, Vector2 b) => a.X * b.X + a.Y * b.Y;

    public override string ToString() => $"({X:F2}, {Y:F2})";
}

public readonly struct LineSegment
{
    public readonly Vector2 Start;
    public readonly Vector2 End;

    public LineSegment(Vector2 start, Vector2 end) => (Start, End) = (start, end);
    public float Length => (End - Start).Magnitude;
    public Vector2 Midpoint => new((Start.X + End.X) / 2, (Start.Y + End.Y) / 2);
}

var v1 = new Vector2(3, 4);
var v2 = new Vector2(7, 1);

Console.WriteLine($"v1: {v1}, magnitude: {v1.Magnitude:F2}");
Console.WriteLine($"v1 normalized: {v1.Normalized}");
Console.WriteLine($"v1 + v2: {v1 + v2}");
Console.WriteLine($"v1 * 2: {v1 * 2}");
Console.WriteLine($"Dot product: {Vector2.Dot(v1, v2):F2}");

var segment = new LineSegment(v1, v2);
Console.WriteLine($"\nLine segment length: {segment.Length:F2}");
Console.WriteLine($"Midpoint: {segment.Midpoint}");

Expected output:

v1: (3.00, 4.00), magnitude: 5.00
v1 normalized: (0.60, 0.80)
v1 + v2: (10.00, 5.00)
v1 * 2: (6.00, 8.00)
Dot product: 25.00

Line segment length: 5.00
Midpoint: (5.00, 2.50)

What's Next

You have mastered structs in C# including value-type semantics, readonly structs, ref structs, and record structs. The next lesson covers strings: immutability, StringBuilder, interpolation, and verbatim strings.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro