“out”的意思,粗略地说,“只出现在输出位置”。
“in”的意思,粗略地说,“只出现在输入位置”。
真实的故事比这要复杂一些,但是选择关键字是因为大多数时候都是这种情况。
考虑接口的方法或委托表示的方法:
delegate void Foo</*???*/ T>(ref T item);
T 是否出现在输入位置?是的。调用者可以通过 item 传入一个 T 的值;被调用者 Foo 可以阅读。因此 T 不能被标记为“out”。
T 是否出现在输出位置?是的。被调用者可以将新值写入项目,然后调用者可以读取该值。因此,T 不能被标记为“in”。
因此,如果 T 出现在“ref”形式参数中,则不能将 T 标记为 in 或 out。
让我们看一些事情如何出错的真实例子。假设这是合法的:
delegate void X<out T>(ref T item);
...
X<Dog> x1 = (ref Dog d)=>{ d.Bark(); }
X<Animal> x2 = x1; // covariant;
Animal a = new Cat();
x2(ref a);
好吧,我的猫,我们刚刚做了一次猫叫。“出”是不合法的。
“在”呢?
delegate void X<in T>(ref T item);
...
X<Animal> x1 = (ref Animal a)=>{ a = new Cat(); }
X<Dog> x2 = x1; // contravariant;
Dog d = new Dog();
x2(ref d);
我们只是把一只猫放在一个只能容纳狗的变量中。T 也不能标记为“in”。
输出参数呢?
delegate void Foo</*???*/T>(out T item);
? 现在 T 只出现在输出位置。将 T 标记为“out”是否合法?
抱歉不行。“out”实际上与幕后的“ref”没有什么不同。“out”和“ref”的唯一区别是编译器禁止在被调用者分配之前从out参数读取,并且编译器要求在被调用者正常返回之前进行赋值。使用 C# 以外的 .NET 语言编写此接口的实现的人将能够在项目初始化之前读取它,因此它可以用作输入。因此,在这种情况下,我们禁止将 T 标记为“out”。这很遗憾,但我们无能为力;我们必须遵守 CLR 的类型安全规则。
此外,“out”参数的规则是它们在写入之前不能用于输入。没有规定它们在写入后不能用于输入。假设我们允许
delegate void X<out T>(out T item);
class C
{
Animal a;
void M()
{
X<Dog> x1 = (out Dog d) =>
{
d = null;
N();
if (d != null)
d.Bark();
};
x<Animal> x2 = x1; // Suppose this were legal covariance.
x2(out this.a);
}
void N()
{
if (this.a == null)
this.a = new Cat();
}
}
我们再次制作了猫吠。我们不能让 T “出局”。
以这种方式使用out参数进行输入是非常愚蠢的,但是合法的。
更新:C# 7 添加了in
作为形式参数声明,这意味着我们现在拥有这两者in
并out
意味着两件事;这会造成一些混乱。让我澄清一下:
in
,out
并且ref
在参数列表中的形式参数声明上表示“此参数是调用者提供的变量的别名”。
ref
意思是“被调用者可以读取或写入别名变量,并且在调用之前必须知道它已被赋值。
out
表示“被调用者必须通过别名写入别名变量才能正常返回”。这也意味着被调用者在写入别名之前不得通过别名读取别名变量,因为该变量可能未明确分配。
in
表示“被调用者可以读取别名变量,但不会通过别名写入它”。的目的in
是解决一个罕见的性能问题,即必须“按值”传递大型结构,但这样做代价高昂。作为实现细节,in
参数通常通过指针大小的值传递,这比按值复制要快,但在取消引用时要慢。
- 从 CLR 的角度来看,
in
和out
都是ref
一回事;关于谁在什么时间读写什么变量的规则,CLR 不知道也不关心。
- 由于是 CLR 执行有关方差的规则,因此适用的规则
ref
也适用于in
和out
参数。
相反,in
在out
类型参数声明上,分别表示“此类型参数不得以协变方式使用”和“此类型参数不得以逆变方式使用”。
如上所述,我们为这些修饰符选择in
andout
是因为如果我们看到IFoo<in T, out U>
thenT
用于“输入”位置并U
用于“输出”位置。虽然这并不完全正确,但在 99.9% 的用例中它是一个有用的助记符,这已经足够正确了。
不幸的是,这interface IFoo<in T, out U> { void Foo(in T t, out U u); }
是非法的,因为它看起来应该起作用。它无法工作,因为从 CLR 验证者的角度来看,它们都是ref
参数,因此是可读写的。
这只是那些在逻辑上应该一起工作的两个功能由于实现细节原因而不能很好地一起工作的奇怪的、意想不到的情况之一。