简而言之,根据ECMA-335规范,这是正确的,令人困惑的是,在某些情况下它确实有效
假设我们有两个变量
IInterface<Animal> i1 = anInterfaceAnimalValue
IInterface<Cat> i2 = anInterfaceCatValue
我们可以进行这些调用。
i1.Baz(anAnimal, j => 5);
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
i1.Baz(aCat, j => 5);
i1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));
i2.Baz(aCat, j => 5);
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泛型类型定义
在声明中,通用参数的作用域为:
- snip...
- 所有成员(实例和静态字段、方法、构造函数、属性和事件),除了嵌套类。 [注意:C#允许从封闭类中使用通用参数以在嵌套类中使用,但会将所需的任何额外通用参数添加到元数据中的嵌套类定义。 end note]
嵌套类型,例如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);
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input);
void BarNestIn(FooNestIn input);
void BarNestOut(FooNestOut input);
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
public interface IInterfaceOut<out T>
{
void BarIn(FooIn<T> input);
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input);
void BarNestIn(FooNestIn input);
void BarNestOut(FooNestOut input);
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
暂时先采用 IInterfaceIn
。
拿无效的 BarIn
举例。它使用了协变的类型参数 FooIn
。
现在,如果我们有一个 anAnimalInterfaceValue
,那么我们就可以使用 FooIn<Animal>
参数调用 BarIn()
。这意味着委托接受一个 Animal
参数。如果我们将其转换为 IInterface<Cat>
,那么我们就可以使用 FooIn<Cat>
调用它,这个方法需要一个 Cat
参数,并且底层对象不期望这样一个严格的委托,而是期望能够传递任何一个 Animal
。
因此,BarIn
只能使用相同或者更少派生的类型,因此它不能接收可能更多派生的 IInterfaceIn
的 T
。
BarOut
是有效的,因为它使用反变的 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
,意思是“将负号变为正号,将正号变为负号”。
方法。 如果一个方法签名 tmeth(t_1,...,t_n)
相对于 S
是有效的当且仅当:
- 其结果类型签名
t
相对于 S
是有效的;并且
- 每个参数类型签名
t_i
相对于 ¬S
是有效的。
- 每个方法泛型参数约束类型
t_j
相对于 ¬S
是有效的。【注:换句话说,结果表现为协变,而参数则表现为逆变……】
因此,我们要翻转方法参数中使用的类型的变异性(variance)。
所有这一切的结果是,在方法参数位置上不可能使用嵌套的协变型或逆变型,因为所需的变异性已经被翻转了,因此不会匹配。无论我们如何做,都行不通。
相反,在返回位置上使用委托总是奏效的。
IInterfaceOut
的最后一部分有什么问题,但某些地方感觉不太对。为什么IInterfaceOut.BarIn
能工作,但IInterfaceOut.BarNest
和BarNestIn
却不能?我甚至认为自己也没有完全理解它为什么这样做。如果您愿意,可以直接编辑我的帖子。我最初还更强调了嵌套重新声明的内容,但后来意识到这实际上基本上是无关紧要的。 - Charlieface