C# scoped ref: Controlling Reference Lifetimes
C# scoped ref: Learn how the scoped modifier in C# restricts reference lifetimes, prevents unsafe escapes, and improves safety with ref structs like Span<T>.
The scoped modifier in C# controls how far a reference may escape the current method. This article explains scoped ref syntax, how the compiler uses it during escape analysis, and where it applies to ref struct types such as Span<T>.
In C# 11 and later, scoped gives developers explicit control over the lifetime of references passed to methods. Applied to a ref parameter, it tells the compiler that the reference must not escape the method's scope. The same idea is used for scoped ref fields inside ref struct types. Without scoped, the compiler applies conservative escape analysis that can reject valid code or force heap allocation. Understanding scoped ref lets you write code that works with stack-only types while preserving the compiler's safety guarantees.
The Problem: Ref Structs and Escape Analysis
ref struct types like Span<T> and ReadOnlySpan<T> can only live on the stack. The compiler enforces this by tracking where references to these types are stored and how they flow through method calls. This is called escape analysis. If a method takes a ref struct parameter and then stores that reference in a field or returns it, the compiler must ensure the underlying memory is still valid.
Consider a simple method that tries to return a Span created from a local array:
public Span<int> CreateSpan() { int[] array = new int[10]; return array.AsSpan(); }
This compiles because the array is heap-allocated, so the span remains valid after the method returns. But if the span points to stack memory, such as a stackalloc buffer, returning it would be illegal:
public Span<int> CreateStackSpan() { Span<int> span = stackalloc int[10]; return span; // Compiler error: CS8352 }
The compiler rejects this because the stack memory is reclaimed when the method exits. Escape analysis determines when a reference can safely leave a method. The scoped modifier gives you a way to tell the compiler that a reference is intentionally limited to the current method body, which relaxes some of the conservative restrictions.
Declaring a Scoped Ref Parameter
When a method accepts a ref parameter that should not be stored or returned, you can mark it as scoped ref. This tells the compiler that the parameter's lifetime is limited to the method's execution. For example:
public void ProcessSpan(scoped ref Span<int> buffer) { // Use buffer, but do not store it or return it. buffer[0] = 42; }
Without scoped, the compiler assumes the ref parameter might escape, so it requires the caller to provide a reference that is safe to store. This can reject valid callers or force them to use heap-allocated data. By marking the parameter as scoped, you promise that the method will not let the reference outlive the call. This allows the caller to pass stack-allocated spans or other short-lived references.
The scoped modifier can also be applied to in parameters and to ref fields inside ref struct types. out parameters are intended to pass values back to the caller, so they are not the usual place for a lifetime restriction.
Scoped Ref Fields in Ref Structs
ref struct types can contain ref fields, but those fields have strict lifetime rules. A ref field must either be scoped or the containing type must enforce that the field does not outlive the reference it stores. The scoped modifier on a ref field indicates that the field's lifetime is tied to the current method rather than to the original reference's source.
public ref struct BufferWrapper { private scoped ref int _first; public BufferWrapper(scoped ref int first) { _first = ref first; } public int GetFirst() => _first; }
Here, _first is a scoped ref field. The compiler knows that a reference to the field's storage cannot be returned from the current method. This allows the wrapper to be constructed from a local variable when the wrapper is used within the same enclosing scope. Without scoped, the compiler would require the ref field to be assigned only from references known to live at least as long as the containing instance, which often forces the use of heap-allocated objects and defeats the purpose of a ref struct.
How Scoped Ref Affects Method Calls and Returns
When a method takes a scoped ref parameter, the compiler allows the method to call other methods with the reference, but it cannot store the reference in a field or return it. This is a one-way restriction: the reference can flow deeper into the call stack but not upward. A deeper method must also declare the matching parameter as scoped so the compiler can preserve the lifetime promise.
For example:
public void Process(scoped ref Span<int> data) { // Allowed: data flows into another method with a scoped parameter. Helper(ref data); // Not allowed: data cannot be assigned to a field. // _stored = ref data; } private void Helper(scoped ref Span<int> data) { }
If you need to return a reference that was passed in, you cannot mark the parameter as scoped. Instead, you must rely on the caller's reference being safe to escape. The scoped modifier is purely a promise about the method's behavior, and the compiler enforces it.
One common use case is with stackalloc buffers. A method that initializes a stack-allocated span and passes it to a processing function can use scoped ref to avoid unnecessary heap allocation:
public void ProcessStackData() { Span<int> buffer = stackalloc int[10]; ProcessSpan(ref buffer); } public void ProcessSpan(scoped ref Span<int> data) { // data is valid only during this call. data[0] = 1; }
Without scoped, the compiler might reject the call because buffer is a stack-allocated span and the method could store the reference. With scoped, the compiler knows the reference cannot escape, so the call is safe.
Common Compiler Errors and Their Fixes
The most common error when working with scoped ref occurs when a method tries to return a reference that is scoped. For example:
public ref int GetRef(scoped ref int value) { return ref value; // error CS8352: scoped ref may not be returned }
Here, the parameter could be an alias to a local in the caller. Because the scoped modifier says the reference will not escape the method, returning it contradicts that restriction. To return a reference, remove scoped and ensure the caller's reference is safe to escape.
Another error occurs when a method tries to store a scoped ref struct value in a field:
public ref struct Container { private Span<int> _field; public void SetField(scoped ref Span<int> value) { _field = value; // error CS8350: scoped ref struct may not be stored in a field } }
value has a lifetime limited to the current method call. Assigning it to _field would let it outlive the call, so the compiler rejects it. To fix this, either remove scoped and require a caller-provided reference that is safe to store, or restructure the code so the span is consumed during the call instead of stored.
Understanding these errors helps you decide where to apply scoped and where to avoid it.
Performance and Safety Considerations
scoped ref is primarily a safety feature, but it also has performance implications. By marking a parameter as scoped, you allow the compiler to avoid defensive copies and heap allocations that would otherwise be required to satisfy escape analysis. For example, a method that processes a Span without scoped might force the caller to heap-allocate data if the compiler cannot prove the span is safe to store. With scoped, the compiler knows the reference will not escape, so the caller can pass stack-allocated data directly.
The annotation itself is compile-time only; it does not change the generated code. It only affects the compiler's analysis. Because the compiler enforces the restriction, code that compiles will not violate the scoped promise.
From a maintenance perspective, scoped makes the lifetime contract explicit. A method signature that includes scoped ref immediately tells the reader that the reference is temporary and must not be stored. This reduces the chance of future modifications accidentally introducing a lifetime bug.
When to Use Scoped Ref (and When Not To)
Use scoped ref when you have a method that accepts a ref parameter whose type is a ref struct and you know the method will not store or return that reference. This commonly occurs in low-level utility methods, parsing routines, and algorithms that process spans without retaining them.
Do not use scoped ref if the method needs to return the reference or store it in a field that outlives the call. In those cases, the reference must be safe to escape, and scoped would be a restriction that the compiler rejects.
Another situation where scoped is useful is with ref fields in ref struct types. If a ref struct only uses a reference during its own lifetime, marking the field as scoped allows more flexible construction from stack-allocated data. This is common in custom enumerators or lightweight wrappers around spans.
When you are unsure whether a reference might escape, leave out scoped. The compiler's default escape analysis is conservative and will reject unsafe code. Adding scoped is a deliberate decision that should be based on the method's actual behavior, not as a workaround for a compiler error. If you get an error about a ref struct escaping, examine whether the method truly stores or returns the reference. If it does, you need to redesign the code rather than force a scoped annotation.