而 out 则多用于传递函数的结果,非常类似 C/C++ 以及 COM 中返回调用是否成功,而实际数据则通过参数里的指针传出的方法:
bool TryGetValue(out int x)
{
if (...)
{
x = default;
return false;
}
x = 42;
return true;
}
if (TryGetValue(out int x))
{
Console.WriteLine(x);
}
复制代码
in 则是在 C# 7 才引入的,相对于 ref 而言,in 提供了只读引用的功能。通过 in 传入的参数会通过引用方式进行只读传递,类似 C++ 中的 const T*。
为了提升 in 的易用性,C# 为其加入了隐式引用传递的功能,即调用时不需要在调用处写一个 in,编译器会自动为你创建局部变量并传递对该变量的引用:
你会发现这时代码无法编译了。
因为 stackalloc 出来的东西仅在 Test 函数的生命周期内有效,但是我们有可能在 Foo 的构造函数中将 ref int x 这一引用存储到 Foo 的字段中,然后由于 Test 方法返回了 foo,这使得 foo 的生命周期被扩展到了调用 Test 函数的函数上,有可能导致本身应该在 Test 结束时就释放的 x[0] 的生命周期被延长,从而出现无效引用。因此编译器拒绝编译了。
你可能会好奇,编译器在理论上明明可以检测到底有没有实际的代码在字段中保存了引用,为什么还是直接报错了?这是因为,如果需要检测则需要实现复杂度极其高的过程分析,不仅会大幅拖慢编译速度,而且还存在很多无法静态处理的边缘情况。
那要怎么处理呢?这个时候 scoped 就出场了:
Foo Test()
{
Span<int> x = stackalloc[] { 1, 2, 3, 4, 5 };
Foo foo = new Foo(ref x[0]);
return foo;
}
ref struct Foo
{
public Foo(scoped ref int x)
{
x++;
}
}
复制代码
我们只需要在 ref 前加一个 scoped,显式标注出 ref int x 的生命周期不会超出该函数,这样我们就能通过编译了。
此时,如果我们试图在字段中保存这个引用的话,编译器则会有效的指出错误:
ref struct Foo
{
public ref int X;
public Foo(scoped ref int x)
{
X = ref x; // 错误
}
}
复制代码
同样的,我们还可以在局部变量中配合 ref 或者 ref readonly 使用 scoped:
Span<int> a = stackalloc[] { 1, 2, 3, 4, 5 };
scoped ref int x = ref a[0];
scoped ref readonly int y = ref a[1];
foreach (scoped ref int i in a) i++;
foreach (scoped ref readonly int i in a) Console.WriteLine(i); // 2 3 4 5 6