TL;DR;根据 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。这两个委托是相同的签名这一事实并没有从他们身上带走是不同的类型。
让我们再深入一点
让我先说两点:
首先,请记住,接口中的协变泛型类型可以出现在输出位置(这允许更多派生类型),而在中是逆变的输入 位置(允许更基本的类型)。
Covariance and contravariance in generics
一般来说,协变类型参数可以作为委托的返回类型,逆变类型参数可以作为参数类型。对于接口,协变类型参数可以作为接口方法的返回类型,逆变类型参数可以作为接口方法的参数类型。
使用您传入的参数的类型参数,这有点令人困惑:如果T 是协变的(输出),则函数可以使用void (Action<T>),它看起来像是一个输入,并且可以接受更多派生的委托。也可以返回Func<T>。
如果T 是逆变的,则相反。
请参阅this excellent post by the great Eric Lippert 和on the same question by Peter Duniho 了解有关这一点的进一步说明。
其次,ECMA-335,它定义了 CLI 的规范,它说以下(我的粗体字):
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,那么我们可以使用FooIn<Animal> 参数调用BarIn()。这意味着委托接受Animal 参数。如果我们然后将它转换为IInterface<Cat>,那么我们可以使用FooIn<Cat> 调用它,它要求 类型为Cat 的参数,并且底层对象并不期望如此严格的委托,它期望能够通过 any Animal.
因此BarIn 只能使用与声明的类型相同或 派生的类型,因此它无法接收IInterfaceIn 的T,它可能以更多衍生。
但是,
BarOut 是有效的,因为它使用 FooOut,它有一个 contra-变体 T。
现在让我们看看FooNestIn 和FooNestOut。这些实际上重新声明了封闭类型的T 参数。 FooNestOut 无效,因为它在输出位置使用协变体 in T。 FooNestIn 虽然有效。
让我们继续讨论BarNest、BarNestIn 和BarNestOut。这些all 无效,因为它们使用具有 co 变体泛型参数的委托。 这里的关键是我们不关心委托是否真的在必要的位置使用了类型参数,我们关心的是委托的泛型参数的方差是否与我们提供的类型匹配。强>
啊哈,你说,但是为什么 IInterfaceOut 嵌套参数不起作用?
让我们再看一下 ECMA-335,它谈到泛型参数是有效的,并断言泛型类型的每个部分都必须是有效的(我的粗体,S 指的是泛型类型,例如 List<T>,@ 987654368@ 表示类型参数,var 表示相应参数的in/out):
II.9.7 会员签名的有效性
给定带注释的泛型参数S = <var_1 T_1, ..., var_n T_n>,我们定义类型定义的各个组件相对于S 有效的含义。我们在注解上定义了一个否定操作,写成¬S,意思是“将负数翻转为正数,将正数翻转为负数”
方法。方法签名tmeth(t_1,...,t_n) 相对于S 有效
- 其结果类型签名
t相对于S有效;和
- 每个参数类型签名
t_i 对¬S 有效。
- 每个方法泛型参数约束类型
t_j 对¬S 有效。 [注意:换句话说,结果的行为是协变的,而参数的行为是逆变的……
所以我们翻转方法参数中使用的类型的方差。
所有这一切的结果是,在方法参数位置使用嵌套的 co-或逆变类型是永远有效的,因为所需的 variance 被翻转,因此不会匹配。不管我们怎么做,都行不通。
相反,在返回位置使用代理总是有效的。