Home > Net >  C# 11 escape rules for ref parameters: ref int vs Span<int>
C# 11 escape rules for ref parameters: ref int vs Span<int>

Time:12-26

Why does the following code not compile in C# 11?

// Example 1 - fails
class C {
    public Span<int> M(ref int arg) {
        Span<int> span;
        span = new Span<int>(ref arg);
        return span;
    }
}

It produces two compile errors:

error CS9077: Cannot return a parameter by reference 'arg' through a ref parameter; it can only be returned in a return statement.

error CS8347: Cannot use a result of 'Span.Span(ref int)' in this context because it may expose variables referenced by parameter 'reference' outside of their declaration scope.

Neither of them makes sense to me: my code doesn't try to return arg by a ref parameter, and it can't expose variables referenced by arg outside of their declaration scope.

By comparison, the following two pieces of code compile successfully:

// Example 2 - succeeds
class C {
    public Span<int> M(ref int arg) {
        Span<int> span = new Span<int>(ref arg);
        return span;
    }
}
// Example 3 - succeeds
class C {
    public Span<int> M(Span<int> arg) {
        Span<int> span;
        span = new Span<int>(ref arg[0]);
        return span;
    }
}

My intuition is that Span<int> internally holds a ref field of type int, so the escape rules should work the same for Examples 1 and 3 above (which, apparently, they do not).

I made an analogous experiment with a ref struct explicitly holding a ref field:

ref struct S {
    public ref int X;
}

Now, the following code fails to compile:

// Example 4 - fails
class C {
    public S M(ref int arg) {
        S instance;
        instance.X = ref arg;
        return instance;
    }
}

It produces the following error, which at least makes slightly more sense to me:

error CS9079: Cannot ref-assign 'arg' to 'X' because 'arg' can only escape the current method through a return statement.

By comparison, the following two pieces of code compile successfully (with the definition of S above):

// Example 5 - succeeds
class C {
    public S M(ref int arg) {
        S instance = new S() { X = ref arg };
        return instance;
    }
}
// Example 6 - succeeds
class C {
    public S M(S arg) {
        S instance;
        instance.X = ref arg.X;
        return instance;
    }
}

In particular, if arg can only escape the current method through a return statement, as in the error message for Example 4 above, while doesn't the same hold for arg.X in Example 6?

I tried to find the answer in the documentation for low level struct improvements, but I failed. Moreover, that documentation page seems to contradict itself in several places.

CodePudding user response:

are you sure you are using C# 11? Using linqpad with .Net 7 your "fails to compile" example worked fine for me:

Compiles Fine

Update: doesn't compile if using the daily build of the Rosyln compiler

My new hypothesis is that the spec actually got tighter... and ex1 and ex2 should both fail, but they have not accounted for the ex2 syntax where its not triggering when it should (for the reason Marc G pointed out) so might be worth filing a bug report on this :-)

CodePudding user response:

This very closely related issue had been reported to the Roslyn team before and closed as "by design".

The issue is that the compiler associates an internal scope with a variable of a ref struct type such as a span<>. This scope is decided at the moment the variable is declared.

Later on, when assignments happen, the internal scopes are compared.

Although both the uninitialized local span<> variable as well as the span<> variable wrapped around the ref argument should be returnable from the method, the compiler seems to think otherwise.

I would report this concrete example to the Roslyn team and see what they say about it.

Previous musings:


It is an issue of scope. Look at this more explicit example:

public void M()
{
    Span<int> spanOuter;
    {
        int answer = 42;
        spanOuter = new Span<int>(ref answer); // Compiler error
    }

    Console.WriteLine(spanOuter[0]); // Would access answer 42 which
                                     // is already out of scope
}

The new Span<int>() created has a narrower scope than the variable spanOuter. You cannot assign spans to another span with a broader scope because that could mean that the referenced data they hold is accessed after they don't exist any more. In this example, the answer variable goes out of scope before spanOuter[0] is accessed.

Let's remove the curly braces:

public void M()
{
    Span<int> spanOuter;
    int answer = 42;
    spanOuter = new Span<int>(ref answer); // Compiler error
    Console.WriteLine(spanOuter[0]); 
}

Now this should in theory work because the answer variable is still in scope at the Conole.WriteLine. The compiler still doesn't like it. Although there are no curly braces, the spanOuter variable still has a broader scope than at the new Span<int>() expression because its declaration happens on its own on a previous line.

When checking for breadth of scope, the compiler seems to be very strict and difference in scope just because of the separate variable declaration seems to be enough to not allow the assignment.


Even when we move the answer variable at the very beginning so that it basically has the same scope as an argument has, it is still not allowed.

public void M()
{
    int answer = 42;
    Span<int> spanOuter;
    spanOuter = new Span<int>(ref answer); // Compiler error
    Console.WriteLine(spanOuter[0]); 
}

The compiler seems to treat arguments just like local variables for this check. I agree that the compiler could be a bit more clever, look at the precise scope of the referenced data and allow some more cases, but it just doesn't do that.


Specifically, the compiler seems to have a special treatment when the target span variable is uninitialized as seen by the compiler.

public void M(ref int a)
{
    int answer = 42;

    Span<int> spanNull = null;
    Span<int> spanImplicitEmpty;
    Span<int> spanExplicitEmpty = Span<int>.Empty;
    Span<int> spanInitialized = new Span<int>(ref answer);

    Span<int> spanArgument = new Span<int>(ref a);

    spanNull            = spanArgument; // Compiler Error
    spanExplicitEmpty   = spanArgument; // Compiler Error
    spanImplicitEmpty   = spanArgument; // Compiler Error
    spanInitialized     = spanArgument; // Works
}

The same applies when using a return value:

public Span<int> M(ref int a)
{
    int answer = 42;

    Span<int> spanNull = null;
    Span<int> spanImplicitEmpty;
    Span<int> spanExplicitEmpty = Span<int>.Empty;
    Span<int> spanInitialized = new Span<int>(ref answer);
    
    Span<int> spanInitializedAndThenNull = new Span<int>(ref answer);
    spanInitializedAndThenNull = null;

    Span<int> spanArgument = new Span<int>(ref a);

    spanNull                    = spanArgument; // Compiler Error
    spanExplicitEmpty           = spanArgument; // Compiler Error
    spanImplicitEmpty           = spanArgument; // Compiler Error
    spanInitialized             = spanArgument; // Works
    spanInitializedAndThenNull  = spanArgument; // Works

    return spanArgument;
}
  • Related