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. 

To download the source code for this article, you can visit our GitHub repository.

Let’s begin.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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:

Error - Exposed Referenced Variables

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#.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!