25

While looking at the source code of the new DefaultInterpolatedStringHandler I noticed that the ReadOnlySpan was annotated with the scoped keyword. The only documentation I could find was here. However, I wasn't able to figure what the practical difference between the following code snippets would be. I assume that with the scoped keyword the parameter is not allowed to be passed to a called method, nor to be returned. Even if this turns out to be true, what would be the practical use of it?

public void AppendFormatted(ReadOnlySpan<char> value) 
{
  // Omitted for brevity
}

// vs

public void AppendFormatted(scoped ReadOnlySpan<char> value) 
{
  // Omitted for brevity
}
3
  • 3
    I'm curious why you linked to scoped locals piece of documentation when the next section is scoped parameter values which appears to be exactly what you're asking about. Commented Aug 16, 2022 at 13:52
  • @Damien_The_Unbeliever Well you are damn right to ask so. I actually believed this was the right section, as it already mentions scoped parameters at least in code. Guess I glimpsed over to quickly. And from what I can tall my initial assumption holds true. Thanks ;)
    – Twenty
    Commented Aug 16, 2022 at 13:59
  • If I am interpreting your assumption, it is actually the opposite. Today, you cannot pass a locally declared ref struct to the method of an inbound ref struct since the lifetime of that inbound ref struct cannot be determined and there is concern of that method capturing the locally declared ref struct. This is a severe limitation when needing to pass around ref struct-based buffers. The scoped keyword enables that behavior in cases where the the locally declared ref struct is never captured/returned by the utilizing method, taking the scoped ref struct.
    – David L
    Commented Aug 16, 2022 at 17:48

1 Answer 1

26

As I mentioned in my comment, the purpose of the scoped keyword for ref struct parameters is to allow stack allocated local variables to be passed to methods that do not capture the inbound ref struct or return the inbound ref struct from the method.

In the following highly trivialized example of the source you linked:

public ref struct Test
{
    private Span<char> _chars;
    private int _pos;
    
    public Test()
    {
        _chars = new char[3];   
        _pos = 0;
    }
    
    public void AppendFormatted(ReadOnlySpan<char> value)
    {
        // Fast path for when the value fits in the current buffer
        if (value.TryCopyTo(_chars.Slice(_pos)))
        {
            _pos += value.Length;
        }
    }
    
    public Span<char> GetBuffer() => _chars.Slice(0, _pos);
}

if called from the following:

RunSpan();

void RunSpan()
{
    var test = new Test();
    Span<char> valuesToCopy = stackalloc char[1] { 'd' };
    test.AppendFormatted(valuesToCopy);
}

it will throw the following errors

error CS8352: Cannot use variable 'valuesToCopy' in this context because it may expose referenced variables outside of their declaration scope error

CS8350: This combination of arguments to 'Test.AppendFormatted(ReadOnlySpan)' is disallowed because it may expose variables referenced by parameter 'value' outside of their declaration scope

However, as soon as you add the scoped keyword AppendFormatted(scoped ReadOnlySpan<char> value) you can call the method.

Note that scoped does NOT allow you to incorrectly return the parameter from the method! The following:

public ref struct Test
{
    private Span<char> _chars;
    private int _pos;
    
    public Test()
    {
        _chars = new char[3];   
        _pos = 0;
    }
    
    public ReadOnlySpan<char> AppendFormattedBad(scoped ReadOnlySpan<char> value) => value;
    
    public Span<char> GetBuffer() => _chars.Slice(0, _pos);
}

still returns the following error:

error CS8352: Cannot use variable 'ReadOnlySpan' in this context because it may expose referenced variables outside of their declaration scope

This functionality is tremendously useful when dealing with ref structs, since you often need to stackalloc a buffer that is passed to the method of a ref struct.

2
  • 3
    This is interesting. Your example demonstrates the issue, but if I change the constructor to public Test(Span<char> chars) { _chars = chars; ... }, scoped is not needed to compile. I don't see why. Do you know? Thanks! Commented Nov 13, 2022 at 15:01
  • 5
    @NickStrupat it is because the constructor automatically creates a sense of "scope" for the lifetime of the buffer...it cannot live beyond the instance of Test itself. However, when passing it to a method, the compiler no longer is able to safely infer the lifetime of the buffer. There is a possibility that the method will store the ref struct away beyond the stack of the method invocation and it flat out disallows that usage. scoped allows us to now tell the compiler that the method will behave in the same manner as the constructor.
    – David L
    Commented Nov 14, 2022 at 13:29

Not the answer you're looking for? Browse other questions tagged or ask your own question.