经过几个小时的谷歌搜索,我仍然找不到答案。每个人都跳过那一点。
所以我有一个像这样的引用结构
ref struct RefStruct
{
public ref int X;
}
我尝试这样使用它
void f()
{
var x = 100;
var s = new RefStruct();
s.X = ref x;
}
这给了我一个编译器错误:无法将“ref x”引用分配给“s.X”,因为“ref x”的转义范围比“s.X”更窄
这到底是什么意思?它们都在同一范围内。
让我们添加一个构造函数
ref struct RefStruct
{
public ref int X;
public RefStruct(ref int x)
{
X = ref x;
}
}
void f()
{
var x = 100;
var s = new RefStruct(ref x);
s.X = ref x;
}
现在绝对没问题了,
s
范围变窄了……但是改变了什么? x
的范围是如何扩大的?
事情可能更奇怪
void f()
{
var x = 100;
var y = 5;
var s = new RefStruct(ref y);
s.X = ref x;
}
这个也可以。
我真的不明白为什么我必须通过构造函数初始化
ref
字段,直到我能够重新分配它。
阅读介绍ref
字段的
语言提案。
局部变量的默认“转义范围”基于其初始化方式。确切的语义有点奇怪,是围绕 C#10 中引入的语义设计的,以支持返回
Span<T>
的实例。
// safe-to-escape == calling method
Span<int> x = default;
return x; // ok
// safe-to-escape == current method
Span<int> y = stackalloc int[42];
return y; // error
从您的示例来看,即使您没有返回任何
ref struct
变量。编译器正在强制执行“转义范围”规则,就好像您将返回它们一样。
// safe-to-escape == calling method, can be returned
var s = new RefStruct();
// safe-to-escape == current method, can't be returned
var s = new RefStruct(ref x);
当您尝试分配
s.X = ref x
时。左侧将具有上面变量的“转义范围”,但 ref x
将具有“当前方法”的范围。
如果您尝试将“当前方法”作用域分配给“调用方法”作用域,则会引发 CS8374 错误。
但是您可以通过向结构变量添加显式
scoped
修饰符来覆盖默认的转义范围定义;
// safe-to-escape == current method
scoped var s = new RefStruct();
换句话说,编译器根据局部变量的初始化方式向它们添加隐式
scoped
关键字。但您始终可以显式添加关键字。
在撰写本文时,有一个 C# 编译器 issue 来改进 CS8374 错误消息,建议在相关局部变量上使用
scoped
修饰符。
无法将“ref x”引用分配给“s.X”,因为“ref x”的转义范围比“s.X”窄
这里你应该明白的是,
s
的范围更广,因为它是var
而不是ref var
。
很明显为什么
ref x
的范围比var s
窄,它是一个局部变量,s
可能比x
更长寿。
文档说:
编译器确保存储在 ref 字段中的引用不会比其所指对象的寿命更长。
基本上,您发布的最后一个片段对于编译器来说是启发式有效的。
为什么在构造函数中初始化然后赋值有效?这是因为
X
的存储被初始化了。
尝试调试这个编译后的例子来理解,它会在运行时NRE:
var x = 100;
var s = new RefStruct();
s.X = x; // no 'ref' keyword here