引用参数的 C# 11 转义规则:ref int 与 Span<int>

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

提问人:Bartosz 提问时间:12/25/2022 更新时间:12/26/2022 访问量:594

问:

为什么以下代码无法在 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;
    }
}

它会产生两个编译错误:

错误 CS9077:无法通过 ref 参数引用“arg”返回参数;它只能在 return 语句中返回。

错误 CS8347:在此上下文中不能使用“Span.Span(ref int)”的结果,因为它可能会在其声明范围之外公开参数“reference”引用的变量。

它们对我来说都没有意义:我的代码不会尝试通过 ref 参数返回,并且它不能公开其声明范围之外引用的变量。argarg

通过比较,以下两段代码编译成功:

// 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;
    }
}

我的直觉是内部保存着一个类型的 ref 字段,因此转义规则对于上面的示例 1 和 3 应该相同(显然,它们不是)。Span<int>int

我做了一个类似的实验,一个显式保存 ref 字段的 ref 结构:

ref struct S {
    public ref int X;
}

现在,以下代码无法编译:

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

它产生了以下错误,至少对我来说更有意义:

错误 CS9079:无法将“arg”分配给“X”,因为“arg”只能通过 return 语句转义当前方法。

通过比较,以下两段代码编译成功(使用上述定义):S

// 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;
    }
}

特别是,if 只能通过 return 语句转义当前方法,如上面示例 4 的错误消息所示,而示例 6 中的错误消息是否相同?argarg.X

我试图在文档中找到低级结构改进的答案,但我失败了。此外,该文档页面似乎在几个地方自相矛盾。

C 参考 C#-11.0

评论

0赞 NineBerry 12/26/2022
此问题已报告给 Roslyn Github,并作为“按设计”关闭 github.com/dotnet/roslyn/issues/53014
0赞 Bartosz 12/26/2022
好的,这就解释了为什么示例 1 与 2(或 4 与 5)的行为可能不同。但是示例 1 对 3(或 4 对 6)怎么样?例如,示例 4 中的“生存期”似乎比示例 6 中的“生存期”更窄,我不明白。argarg.X
0赞 NineBerry 12/26/2022
我认为 Roslyn 编译器中的某个特定位置可能存在一个错误,其中做出了一个特定的决定,该决定错过了参数上的修饰符,因此将参数视为局部变量而不是可返回的引用。ref

答:

0赞 Abbotware 12/25/2022 #1

您确定使用的是 C# 11 吗?将 linqpad 与 .Net 7 一起使用,您的“编译失败”示例对我来说效果很好:

编译良好

更新:如果使用 Rosyln 编译器的每日构建,则无法编译

我的新假设是,规范实际上变得更加严格......ex1 和 ex2 都应该失败,但它们没有考虑 ex2 语法,其中它没有在应该触发的时候触发(出于 Marc G 指出的原因),所以可能值得就此提交错误报告:-)

评论

0赞 Bartosz 12/25/2022
是的,我正在使用 C# 11 和 .NET 7.0.100。另请参阅 Sharplab
0赞 Abbotware 12/25/2022
好的。 看起来 linkpad 在 Visual Studio 中使用 <LangVersion>preview</LangVersion>,如果您使用该设置,它会编译,但不是 <LangVersion>latest</LangVersion>这让我相信这是当前规范中的一个边缘情况,他们计划更改/放宽您指出的情况的约束
0赞 Bartosz 12/25/2022
我在项目设置中添加了“<LangVersion>preview</LangVersion>,但这不会影响结果。顺便说一句,我的 VS Code(带有 C# 扩展)也无法识别此错误。只有在使用“dotnet build”进行编译后才会发生。
0赞 Abbotware 12/25/2022
LinqPad 能够运行代码,因此似乎需要在 CSProj 文件中打开编译器设置以启用语言规范的宽松版本。话虽如此 - 只为一个 int 做你正在做的事情似乎有点不寻常(我假设“int”是结构的替代品,使示例重现更简单)?
0赞 Bartosz 12/25/2022
我正在 C# 11 中试验 ref 结构和 ref 字段,这是我不理解的行为的最小示例。我希望非 ref 结构类型的 ref parameters/locals/returns/fields 的行为类似于 ref 结构类型的非 ref parameters/locals/returns/fields。此示例显示它们的行为不同。但你可能是对的,这是一些边缘情况,规则仍在改变。
0赞 NineBerry 12/26/2022 #2

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 type such as a . This scope is decided at the moment the variable is declared.ref structspan<>

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 created has a narrower scope than the variable . 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 variable goes out of scope before is accessed.new Span<int>()spanOuteranswerspanOuter[0]

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 variable is still in scope at the . The compiler still doesn't like it. Although there are no curly braces, the variable still has a broader scope than at the expression because its declaration happens on its own on a previous line.answerConole.WriteLinespanOuternew Span<int>()

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 variable at the very beginning so that it basically has the same scope as an argument has, it is still not allowed.answer

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
}

使用返回值时也是如此:

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;
}

评论

0赞 Bartosz 12/26/2022
您的第一个示例很清楚,但与我的示例不同,因为它尝试返回对局部变量的引用。您的第二个示例与我的示例 1 类似,但对我来说更容易理解。“spanOuter”的范围实际上更广——允许返回。例如,将返回类型从 'void' 更改为 'Span<int>',删除最后一行,并添加以下内容:“spanOuter = default;” 和 “return spanOuter”。编译成功。
0赞 Bartosz 12/26/2022
因此,第二个示例失败,因为它尝试将局部变量的 ref 分配给可返回的 span。但是,在我的示例 1 中,将 ref 参数分配给我们要返回的 span。所以我仍然不明白为什么这不起作用。
0赞 NineBerry 12/26/2022
我已经扩展了我的答案。我认为编译器犯了一个错误。仅当目标跨度变量从未被分配有实际跨度时,它才会被阻塞。
0赞 Bartosz 12/26/2022
你的最后一个例子非常有趣。我知道这有效,因为 的“转义范围”是局部的(它捕获对局部变量的引用)。因此,赋值收紧了 的“转义范围”,这是允许的。请注意,即使在赋值之后也无法返回(假设是返回类型而不是 )。但我不明白为什么其他三个赋值会产生编译错误。spanInitialized = spanArgument;spanInitializedspanArgumentspanInitializedSpan<int>void
0赞 Bartosz 12/26/2022
好的,我今天晚些时候会报告。谢谢!