TL;博士; 根据 ECMA-335 规范,这是正确的,令人困惑的是,在某些情况下它确实有效
假设我们有两个变量
IInterface<Animal> i1 = anInterfaceAnimalValue;
IInterface<Cat> i2 = anInterfaceCatValue;
我们可以拨打这些电话
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
i1.Baz(aCat, j => 5);
//this is the same as doing
i1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));
i2.Baz(aCat, j => 5);
//this is the same as doing
i2.Baz(aCat, new IInterface<Cat>.Foo(j => 5));
如果我们现在分配i1 = i2;
那么会发生什么?
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
但是IInterface<Cat>.Baz
(实际的对象类型)不接受IInterface<Animal>.Foo
,它只接受IInterface<Cat>.Foo
。这两个代表是相同的签名这一事实并不能说明它们是不同的类型。
让我们更深入一点
让我以两点作为开头:
首先,请记住,接口中的协变泛型类型可以出现在输出位置(这允许更多派生类型),而反变型可以出现在输入位置(允许更多基类型)。
泛型中的协变和逆变
一般来说,协变类型参数可以作为委托的返回类型,逆变类型参数可以作为参数类型。对于接口,协变类型参数可以作为接口方法的返回类型,逆变类型参数可以作为接口方法的参数类型。
对于您传入的参数的类型参数,这有点令人困惑:如果T
是协变的(输出),则可以使用void (Action<T>)
看起来像输入的函数,并且可以接受更多派生的委托。它也可以返回Func<T>
。
如果T
是逆变的,则相反。
有关这一点的进一步解释,请参阅伟大的 Eric Lippert 的这篇出色的帖子和Peter Duniho 的同一个问题。
其次,定义 CLI 规范的ECMA-335说如下(我的粗体字):
II.9.1 通用类型定义
泛型参数在以下声明的范围内:
- 剪...
- 除嵌套类外的所有成员(实例和静态字段、方法、构造函数、属性和事件)。 [注意:C# 允许在嵌套类中使用来自封闭类的泛型参数,但会将任何必需的额外泛型参数添加到元数据中的嵌套类定义中。尾注]
因此,以Foo
委托为例的嵌套类型实际上在范围内没有泛型T
类型。C# 编译器将它们添加进去。
现在,看下面的代码,我已经注意到哪些行没有编译:
public delegate void FooIn<in T>(T input);
public delegate T FooOut<out T>();
public interface IInterfaceIn<in T>
{
void BarIn(FooIn<T> input); //must be covariant
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut(); //must be covariant
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be covariant
void BarNestIn(FooNestIn input); //must be covariant
void BarNestOut(FooNestOut input); //must be covariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
public interface IInterfaceOut<out T>
{
void BarIn(FooIn<T> input);
FooIn<T> BazIn(); //must be contravariant
void BarOut(FooOut<T> input); //must be contravariant
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be contravariant
void BarNestIn(FooNestIn input); //must be contravariant
void BarNestOut(FooNestOut input); //must be contravariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
让我们暂时坚持下去IInterfaceIn
。
取无效BarIn
。它使用FooIn
,其类型参数是协变的。
现在,如果我们有,anAnimalInterfaceValue
那么我们可以BarIn()
使用FooIn<Animal>
参数调用。这意味着委托接受一个Animal
参数。如果我们然后将其强制转换为,IInterface<Cat>
那么我们可以使用 a 调用它FooIn<Cat>
,它需要一个 type 的参数Cat
,并且底层对象并不期望如此严格的委托,它期望能够传递任何 Animal
.
因此BarIn
,只能使用与声明的类型相同或更少派生的类型,因此它无法接收可能最终派生更多T
的类型。IInterfaceIn
BarOut
但是,它是有效的,因为它使用FooOut
,它有一个逆变器T
。
现在让我们看看FooNestIn
和FooNestOut
。这些实际上重新声明T
了封闭类型的参数。无效,因为它在输出位置FooNestOut
使用协变。虽然是有效的。in T
FooNestIn
让我们继续BarNest
,BarNestIn
和BarNestOut
。这些都是无效的,因为它们使用具有协变泛型参数的委托。这里的关键是我们不关心委托是否真的在必要的位置使用了类型参数,我们关心的是委托的泛型参数的方差是否与我们提供的类型匹配。
啊哈,你说,但是为什么IInterfaceOut
嵌套参数不起作用呢?
让我们再看看 ECMA-335,它谈到泛型参数是有效的,并断言泛型类型的每个部分都必须是有效的(我的粗体,S
指的是泛型类型,例如List<T>
,T
表示类型参数,var
表示in/out
相应的参数):
II.9.7 会员签名的有效性
给定带注释的泛型参数S = <var_1 T_1, ..., var_n T_n>
,我们定义类型定义的各个组件相对于 有效的含义S
。我们在注释上定义了一个否定操作,写成¬S
,意思是“将负数翻转为正数,将正数翻转为负数”</p>
方法。方法签名tmeth(t_1,...,t_n)
对于S
if是有效的
- 它的结果类型签名
t
对于S
; 和
- 每个参数类型签名
t_i
对于¬S
.
- 每个方法泛型参数约束类型
t_j
对于¬S
. [注意:换句话说,结果的行为是协变的,而论点的行为是逆变的......
所以我们翻转了方法参数中使用的类型的方差。
所有这一切的结果是,在方法参数位置使用嵌套的协变或反变类型永远是无效的,因为所需的方差被翻转,因此不会匹配。不管我们怎么做,都行不通。
相反,在返回位置使用委托总是有效的。