Back to Blog
C#

C# ref struct: Constraints and Practical Use

c# ref struct: Learn how C# ref struct types enforce stack-only allocation, their restrictions, and how Span<T> uses them for safe, allocation-free memory access.

ref structSpan<T>stackallocvalue typesC# memory management
Illustration of a C# ref struct as a stack-only value type with Span<T> referencing contiguous memory.

c# ref struct requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

A ref struct in C# is a value type that the compiler restricts to the stack. The restriction exists to support types like Span<T> that reference managed memory without introducing heap allocations or boxing. Understanding this constraint is essential before using ref struct in your own APIs.

The Core Constraint: Stack-Only Allocation

The defining rule of a ref struct is that instances can only live on the stack. The compiler enforces this by rejecting any usage that would place the value on the managed heap. This includes boxing, using the type as a generic type argument, storing it as a field in a class, or capturing it in a lambda or async method. The rationale is that a ref struct often holds a managed pointer to memory that must remain valid only for the duration of the current stack frame. Allowing it to escape to the heap would make lifetime validation impossible.

This stack-only behavior is not a performance hint; it is a hard language rule. If you attempt to box a ref struct or use it in a generic collection, the compiler produces an error. The restriction is what makes types like Span<T> safe and efficient, because they can point to stack memory, unmanaged memory, or managed arrays without risking a dangling reference.

Declaring a ref struct

You declare a ref struct with the ref modifier on the struct keyword. The syntax is straightforward:

public ref struct BufferReader { private ReadOnlySpan<byte> _buffer; private int _position; public BufferReader(ReadOnlySpan<byte> buffer) { _buffer = buffer; _position = 0; } public byte ReadByte() { return _buffer[_position++]; } }

This BufferReader is a ref struct because it contains a ReadOnlySpan<byte> field. Since ReadOnlySpan<T> is itself a ref struct, any struct that holds it must also be a ref struct. The compiler enforces this transitively, so you cannot hide a ref struct inside a regular class or struct.

Where ref structs Are Allowed

ref struct instances can be used as local variables, method parameters, return values, and ref or in arguments. They can also be stored in other ref struct types. For example, you can pass a Span<T> to a method that accepts a ReadOnlySpan<T>, and you can return a Span<T> from a method as long as the underlying memory is still valid. This is common when working with stack-allocated buffers or slices of arrays.

public static Span<int> GetSlice(int[] array, int start, int length) { return array.AsSpan(start, length); }

The returned Span<int> is safe because the array is managed by the garbage collector, and the span carries a reference to the array. For stack-allocated memory, you must ensure the span does not outlive the stack frame that owns the memory.

Span<T> and Memory<T>: The Primary Use Case

The most important ref struct in the .NET ecosystem is Span<T>. It provides a type-safe view over contiguous memory, whether that memory is an array, a string, unmanaged memory, or a stack-allocated buffer. Because Span<T> is a ref struct, it cannot be stored in a field of a class or used in async methods. This limitation is intentional: Span<T> is designed for synchronous, short-lived operations where performance matters.

Memory<T> exists as a companion type that does not have the ref struct restriction. You can store Memory<T> in a class, use it in async methods, or pass it to lambdas. The tradeoff is that Memory<T> may allocate a heap object when it wraps a non-array source, whereas Span<T> is always allocation-free. The general guidance is to use Span<T> for synchronous processing and Memory<T> when you need to store the reference beyond the current call stack.

Performance and Allocation Behavior

The primary performance benefit of ref struct is the elimination of heap allocations and boxing. When you use a Span<T> to slice an array, no new array is allocated; the span simply points to the original memory. This is critical in high-throughput code paths such as parsers, serializers, and network protocol handlers, where avoiding allocations reduces garbage collection pressure.

Another benefit is that ref struct types cannot be boxed, so they never incur the overhead of an object header or a finalizer. They are pure stack values, which the JIT can often keep in registers. However, this also means that a ref struct cannot be used in any context that requires an object, such as object.Equals, GetHashCode, or string interpolation. If you need these operations, you must manually implement them as methods on the ref struct.

It is important to note that ref struct does not automatically make your code faster. The performance gain comes from avoiding allocations and enabling compiler optimizations like inlining and scalar replacement. If you misuse a ref struct by forcing it through reflection or by copying it repeatedly, you may not see any benefit. Measure your specific scenario before assuming a ref struct will improve performance.

Common Pitfalls and Compiler Errors

The compiler is strict about ref struct usage, and you will encounter errors when you try to violate the rules. A common mistake is attempting to use a ref struct as a generic type argument:

List<Span<int>> list = new(); // Error: Span<int> cannot be used as type argument

The compiler rejects this because a generic type could store the value in a heap-allocated field. Similarly, you cannot use a ref struct in an async method because the state machine that implements the method is a heap-allocated object. If you need to process a span asynchronously, you must copy the data into a Memory<T> or a regular array first.

Another pitfall is capturing a ref struct in a lambda or local function that is used as a delegate. The delegate is a heap object, so the compiler disallows the capture. You can still use a ref struct inside a local function that is never converted to a delegate, as long as the local function is called directly and does not outlive the stack frame.

When to Use and When to Avoid

Use a ref struct when you need a lightweight, allocation-free view over contiguous memory and the lifetime is strictly bounded to the current method call. This is common in parsing, binary reading, and low-level data manipulation. If you need to store the data structure in a field of a class, pass it to an async method, or use it in a generic collection, you must use a regular struct or a Memory<T> instead.

Avoid ref struct for simple data containers that do not reference external memory. A regular struct is easier to work with and does not carry the same restrictions. Also avoid creating a ref struct that holds a reference to a managed object if you do not need the stack-only guarantee; the restriction will limit your API design without providing a clear benefit.

Interop and Advanced Scenarios

ref struct is particularly useful when working with unmanaged memory or stack-allocated buffers. You can use stackalloc to create a buffer on the stack and then wrap it in a Span<T>:

Span<byte> buffer = stackalloc byte[256]; FillBuffer(buffer);

The stackalloc expression returns a Span<T>, which is a ref struct. This pattern is common in high-performance code where allocating a small buffer on the heap would be wasteful. The compiler ensures that the Span<T> does not escape the stack frame, so the memory is automatically reclaimed when the method returns.

When interoping with native code, you can use Span<T> to pass a pointer to a contiguous block of memory without pinning the object. For example, you can create a Span<T> from a byte* and pass it to a method that expects a ReadOnlySpan<T>. This avoids the overhead of fixed statements and reduces the risk of memory corruption, because the span carries the length and the compiler checks bounds at runtime.

c# ref struct: Practical Usage and Code Examples | RYUSLOG DEV