C# scoped ref: Controlling Reference Lifetimes
c# scoped ref: Learn how the scoped modifier in C# restricts ref lifetimes, prevents unsafe escapes, and improves safety with ref structs like Span<T>.
c# scoped ref requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In C# 11, the scoped modifier gives developers explicit control over the lifetime of references passed to or returned from methods. When applied to a ref parameter or a ref field, scoped ref tells the compiler that the reference must not escape the method's scope, enabling safer use of ref struct types like Span<T>. Without scoped, the compiler applies conservative escape analysis that often rejects valid code or forces defensive copies. Understanding scoped ref lets you write high-performance code that works with stack-only types while keeping the compiler's safety guarantees intact.
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. It is not allowed on out parameters because out already implies the value is assigned by the method and may escape.
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 containing instance's lifetime, not to the original reference's source.
public ref struct BufferWrapper { private scoped ref int _first; public BufferWrapper(ref int first) { _first = ref first; } public int GetFirst() => _first; }
Here, _first is a scoped ref field. The compiler knows that the reference stored in _first cannot escape the BufferWrapper instance. This means you can create a BufferWrapper from a local variable and pass it around without worrying that the underlying reference might be invalidated.
Without scoped, the compiler would require the ref field to be assigned only from references that are known to live at least as long as the containing instance. That often forces the use of heap-allocated objects, defeating 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 that 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.
For example:
public void Process(scoped ref Span<int> data) { // This is allowed: data is passed to another scoped method. Helper(ref data); // This is not allowed: data cannot be assigned to a field. // _stored = ref data; // error } private void Helper(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 is CS8352, which occurs when you try to return a ref that is scoped to the method. For example:
public scoped ref int GetRef() { int x = 10; return ref x; // error CS8352: Cannot use local 'x' in this context because it may not be returned }
Here, x is a local variable on the stack, and returning a reference to it is unsafe. The compiler prevents this. To fix it, you need to return a reference to a heap-allocated object or a field that outlives the method.
Another error is CS8350, which occurs when you try to assign a scoped ref to a non-scoped field. For instance:
private Span<int> _field; public void SetField(scoped ref Span<int> value) { _field = value; // error CS8350: This ref struct may not be used as an argument }
The scoped parameter cannot be stored because it might point to stack memory that becomes invalid. To fix this, either remove scoped and ensure the caller passes a safe reference, or change the field to be scoped as well (if the containing type is a ref struct).
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 skip defensive copies and avoid 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 copy the span to the heap 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 runtime cost is zero: scoped is a compile-time annotation that does not change the generated code. It only affects the compiler's analysis. However, using scoped incorrectly can lead to runtime crashes if you violate the promise. The compiler enforces the restriction, so as long as your code compiles, the safety is guaranteed.
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 reference to 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 lie 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.