提问人:Bartosz 提问时间:12/25/2022 更新时间:12/26/2022 访问量:594
引用参数的 C# 11 转义规则:ref int 与 Span<int>
C# 11 escape rules for ref parameters: ref int vs Span<int>
问:
为什么以下代码无法在 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 参数返回,并且它不能公开其声明范围之外引用的变量。arg
arg
通过比较,以下两段代码编译成功:
// 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 中的错误消息是否相同?arg
arg.X
我试图在文档中找到低级结构改进的答案,但我失败了。此外,该文档页面似乎在几个地方自相矛盾。
答:
您确定使用的是 C# 11 吗?将 linqpad 与 .Net 7 一起使用,您的“编译失败”示例对我来说效果很好:
更新:如果使用 Rosyln 编译器的每日构建,则无法编译
我的新假设是,规范实际上变得更加严格......ex1 和 ex2 都应该失败,但它们没有考虑 ex2 语法,其中它没有在应该触发的时候触发(出于 Marc G 指出的原因),所以可能值得就此提交错误报告:-)
评论
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 struct
span<>
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>()
spanOuter
answer
spanOuter[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.answer
Conole.WriteLine
spanOuter
new 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;
}
评论
spanInitialized = spanArgument;
spanInitialized
spanArgument
spanInitialized
Span<int>
void
评论
arg
arg.X
ref