Structs

Intro

A struct is a value type in C#. Variables hold the value inline, so assignment copies the value rather than copying a reference. That makes structs a good fit for small, immutable data representing one logical value (coordinates, money amounts, date-time pairs). Struct values can still live on the heap when embedded in heap objects, captured/hoisted, or boxed. Structs implicitly derive from System.ValueType, are always sealed, and cannot participate in class inheritance.

Deeper Explanation

public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

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

var price = new Money(9.99m, "USD");
var copy = price;   // full bitwise copy — independent value

Key properties:

When to use a struct instead of a class:

Struct Modifiers

readonly struct

Fields must be readonly, and properties must be get-only or init-only. The compiler enforces immutability after construction, which reduces defensive copies when the struct is accessed through in parameters or readonly fields.

public readonly struct Vector2
{
    public double X { get; }
    public double Y { get; }

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

    public double Length => Math.Sqrt(X * X + Y * Y);
    public Vector2 Normalize() => new(X / Length, Y / Length);
}

Without readonly, the compiler can make a defensive copy when a non-readonly member is called through a readonly receiver (for example, an in parameter), because it cannot prove the member is mutation-free. With readonly struct and readonly members, these hidden copies are avoided.

ref struct

A ref struct is stack-only — it cannot be boxed or stored on the managed heap. This enables safe, allocation-free wrappers over stack memory.

public ref struct SpanPair
{
    public Span<byte> First;
    public Span<byte> Second;
}

Key restrictions:

C# 13 relaxes some restrictions: ref structs can now implement interfaces (with an allows ref struct anti-constraint) and appear in some generic contexts.

readonly ref struct

Combines both: stack-only and fully immutable. The canonical example is ReadOnlySpan<T>:

public readonly ref struct ReadOnlySpan<T>
{
    // ...internal pointer and length...
}

ReadOnlySpan<char> slice = "Hello, World!".AsSpan(0, 5);

This is the safest struct form — no heap escape, no mutation.

partial struct

Same as partial classes - splits a struct definition across multiple files, and the compiler merges all parts into one type:

// Measurement.cs
public partial struct Measurement
{
    public double Value { get; set; }
}

// Measurement.Validation.cs
public partial struct Measurement
{
    public bool IsValid() => !double.IsNaN(Value);
}

Modifier Compatibility

Modifier combination Allowed?
readonly + ref Yes (readonly ref struct)
readonly + partial Yes
ref + partial Yes
abstract No — structs are implicitly sealed
sealed Redundant — already implicit
static No

Pitfalls

  1. Mutable structs — Assigning a struct to a new variable or returning it from a property copies the value. Mutating the copy does not affect the original, which leads to silent bugs:
struct MutablePoint { public int X; public int Y; }

var list = new List<MutablePoint> { new() { X = 1, Y = 2 } };
// list[0].X = 10;  // Compile error — indexer returns a copy

Mark structs readonly to make this class of bug impossible.

  1. Large struct copies — A struct bigger than about 16 bytes incurs meaningful copy cost every time it is passed by value, returned, or assigned. Use in, ref, or ref readonly to pass large structs without copying.

  2. Default value-type equality can be expensive — If you do not override Equals and GetHashCode, comparison is field-by-field and may be reflection-based depending on runtime/type shape. Always implement IEquatable<T> on public structs used in hot paths or hash-based collections.

  3. Boxing in generic code — Calling through object/interface references boxes structs and negates allocation benefits. Prefer constrained generic calls (where T : IFoo or where T : struct, IFoo) and invoke members on T directly instead of casting to IFoo.

  4. Default constructor gotcha — Before C# 10, structs could not have an explicit parameterless constructor. Even in C# 10+, default(T) and array allocation still zero-initialize without calling the constructor, so the explicit parameterless constructor is not guaranteed to run in every path.

Tradeoffs

Questions


Whats next