In this article, we will discuss the scoped keyword in C#, explore its use case, and examine how it can be advantageous.
The contextual keyword scoped in C# serves as a powerful tool for restricting the lifetime of a value. Employing the scoped modifier allows us to limit the lifetime of a variable to the current method, promoting localized usage.
Let’s begin.
Pre C# 11 Features
To explain what the scoped
keyword does, we have to first talk about ref struct
.
Ref structs were introduced in C# 7.2 mainly for the benefit of the Span<T>
and ReadOnlySpan<T>
structs. A ref struct guarantees that an instance of the type can only reside on the stack and can’t escape to the managed heap. The compiler enforces strict scoping rules to guarantee this constraint.
With the introduction of ref structs, the C# compiler didn’t (and still doesn’t) allow us to pass value types declared on the stack, for example, allocated with stackalloc
, to a method of a ref value type.
Let’s take a look at the following code snippets, to understand why the compiler restricts this operation.
First, let’s define the FastStore<T>()
struct:
public ref struct FastStore<T> { private Span<T> _values; public int Length { get => _values.Length; } public FastStore() { _values = new T[3]; } public void Clone(ReadOnlySpan<T> values) { if (values.Length != 3) throw new ArgumentException($"'{nameof(values)}' must contain 3 items"); values.CopyTo(_values); } }
Note that the Clone()
method has parameter values
defined without any annotations.
Now, in the Program
class, we define the code we need:
public class Program { static void Main(string[] args) { Span<int> heights = stackalloc int[3] { -5, 10, -1 }; new FastStore<int>().Clone(heights); } }
Here, we call the Clone
method on the new FastStore<int>()
instance, but the compiler will generate an error. This operation is not allowed, because the compiler needs to ensure that the stack-allocated memory doesn’t go out of scope before the struct itself. Otherwise, the ref struct would be referring to out-of-scope variables. For classes and structs, this wouldn’t be a problem since they would simply copy the reference or the value from the allocation.
When compiled, our program results in the following compile error:
To fix this problem, C# 11 introduced the scoped
keyword.
Let’s see what this keyword does in our code.
The scoped Keyword
When we apply the scoped
modifier to parameters or locals of type ref struct
, local reference variables, including those declared with the ref
modifier, or parameters marked with the in
, ref
, or out
modifiers, we limit the lifetime of that value to be localized and reduced to the containing method.
Furthermore, methods declared in a struct implicitly apply the scoped
modifier to the this
keyword, as well as to out
parameters and ref
parameters when the type is a ref struct
. Because the modifier allows automatic control of the variable lifetime, it helps to keep our code clean and reliable.
To allow the code from the previous example to work, and leverage the benefits offered by the ref struct, we need to annotate the values
parameter with the scoped
modifier:
public ref struct FastStore<T> { private Span<T> _values; public int Length { get => _values.Length; } public FastStore() { _values = new T[3]; } public void Clone(scoped ReadOnlySpan<T> values) { if (values.Length != 3) throw new ArgumentException($"'{nameof(values)}' must contain 3 items"); values.CopyTo(_values); } }
With this annotation, we enforce a strict and predictable use of the values
parameter within the containing method.
First, we can use the parameter normally as part of an expression or statement within the method; second, we can only pass it to methods that also have their parameters annotated with the scoped
modifier, improving code predictability; and finally, the containing method cannot return the scoped
parameter.
These measures prevent variable lifetime extension, ensuring reliable and clear code.
Conclusion
In this article, we learned about the scoped keyword, introduced in C# 11. This modifier ensures that the lifetime of a value remains localized and contained within the method. We saw that this functionality is particularly useful when dealing with ref structs. This is a niche topic, and developers are unlikely to encounter it frequently in their day-to-day work. Micro-optimizations of this nature are typically more relevant in low-level library optimization scenarios rather than general application development in C#.