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:
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
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:
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:
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:
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:
public static string FormatGuid(Guid id)
{
Span<char> buffer = stackalloc char[36];
id.TryFormat(buffer, out _);
return new string(buffer);
}
Processing binary data:
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:
- You need to hold a
Span<T>orReadOnlySpan<T>as a field - You are writing high-performance parsing or serialisation code
- You want to guarantee zero heap allocations for a particular operation
- You are working with stack-allocated buffers via
stackalloc
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.