Ref Structs in C#: Stack-Only Types for High Performance

Ref structs are a special category of value type that can never leave the stack. They cannot be boxed, cannot be used as fields in classes, and cannot be captured by lambdas. These restrictions sound severe, but they enable something powerful: types that can safely hold references to stack memory, enabling zero-allocation programming patterns.

Why Ref Structs Exist

The most important ref struct in .NET is Span<T>. It represents a contiguous region of memory and can point to stack-allocated memory, heap arrays, or unmanaged memory:

Example.cs
Span<int> stackMemory = stackalloc int[100];
stackMemory[0] = 42;

int[] array = [1, 2, 3, 4, 5];
Span<int> slice = array.AsSpan(1, 3); // [2, 3, 4]

If Span<T> were a regular struct, it could be boxed (moved to the heap) while still pointing at stack memory. When the stack frame unwinds, the span would become a dangling reference. The ref struct restriction prevents this entire category of memory safety bugs.

Declaring a Ref Struct

Example.cs
public ref struct TokenReader
{
    private ReadOnlySpan<char> _remaining;
    private int _position;

    public TokenReader(ReadOnlySpan<char> input)
    {
        _remaining = input;
        _position = 0;
    }

    public ReadOnlySpan<char> ReadNext()
    {
        var start = _position;
        while (_position < _remaining.Length && _remaining[_position] != ' ')
            _position++;

        var token = _remaining[start.._position];

        if (_position < _remaining.Length)
            _position++; // Skip the space

        return token;
    }

    public bool HasMore => _position < _remaining.Length;
}

This TokenReader can process strings without any heap allocations. It works directly over the character data using spans.

The Restrictions

Ref structs come with constraints that enforce stack-only lifetime:

Example.cs
ref struct StackOnly
{
    public int Value;
}

// These are all compile errors:
class Container
{
    StackOnly field;               // Cannot be a field in a class
}

object boxed = new StackOnly();    // Cannot box

async Task UseAsync()
{
    StackOnly s = new();           // Cannot use in async methods
    await Task.Delay(1);           // (compiler cannot guarantee stack lifetime)
}

void WithLambda()
{
    StackOnly s = new();
    Action a = () => Console.WriteLine(s.Value); // Cannot capture in lambda
}

These restrictions exist because the runtime cannot guarantee that a ref struct will outlive the stack frame that created it if it escapes to the heap in any way.

Disposable Ref Structs

Ref structs cannot implement interfaces (they cannot be boxed to an interface reference). However, the compiler recognises a pattern-based Dispose method:

Example.cs
public ref struct SpanWriter
{
    private readonly Span<byte> _buffer;
    private int _offset;

    public SpanWriter(Span<byte> buffer)
    {
        _buffer = buffer;
        _offset = 0;
    }

    public void WriteByte(byte value) => _buffer[_offset++] = value;

    public int BytesWritten => _offset;

    public void Dispose()
    {
        // Clean up if needed — called by 'using' statements
    }
}

// Works with 'using' despite not implementing IDisposable
using var writer = new SpanWriter(stackalloc byte[256]);
writer.WriteByte(0xFF);

Since C# 13, ref structs can implement interfaces including IDisposable, as long as they are never boxed to that interface type.

Practical Uses

Parsing without allocations:

Example.cs
public static bool TryParseCoordinate(ReadOnlySpan<char> input, out double lat, out double lon)
{
    lat = 0;
    lon = 0;

    var commaIndex = input.IndexOf(',');
    if (commaIndex < 0) return false;

    return double.TryParse(input[..commaIndex], out lat)
        && double.TryParse(input[(commaIndex + 1)..], out lon);
}

Stack-allocated buffers:

Example.cs
public static string FormatGuid(Guid id)
{
    Span<char> buffer = stackalloc char[36];
    id.TryFormat(buffer, out _);
    return new string(buffer);
}

Processing binary data:

Example.cs
public ref struct BinaryReader
{
    private ReadOnlySpan<byte> _data;

    public BinaryReader(ReadOnlySpan<byte> data) => _data = data;

    public int ReadInt32()
    {
        var value = BitConverter.ToInt32(_data);
        _data = _data[4..];
        return value;
    }

    public ReadOnlySpan<byte> ReadBytes(int count)
    {
        var result = _data[..count];
        _data = _data[count..];
        return result;
    }
}

When to Use Ref Structs

Ref structs are not everyday types. Use them when:

For most application code, regular structs and classes are appropriate. Ref structs are a tool for library authors and performance-critical code paths where every allocation counts. They represent C#'s commitment to being a language where you can write safe, high-performance code without dropping down to unsafe pointers.