考虑带有委托的逆变接口定义:
public interface IInterface<in TInput>
{
delegate int Foo(int x);
void Bar(TInput input);
void Baz(TInput input, Foo foo);
}
Baz
的定义失败并显示错误:
CS1961
方差无效:类型参数 'TInput
' 必须在 'IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)
' 上协变有效。'TInput
'是逆变的。
我的问题是为什么?乍一看,这应该是有效的,因为Foo
代表与TInput
无关。我不知道是编译器过于保守还是我错过了什么。
请注意,通常不会在接口内声明委托,特别是这不会在早于 C# 8 的版本上进行编译,因为接口中的委托需要默认接口实现。
如果允许此定义,有没有办法破坏类型系统,或者编译器是保守的?
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
,那么我们可以用FooIn<Animal>
参数调用BarIn()
。这意味着委托接受Animal
参数。如果我们随后将其转换为IInterface<Cat>
那么我们可以用FooIn<Cat>
调用它,这需要一个Cat
类型的参数,并且底层对象不需要如此严格的委托,它期望能够通过任何Animal
。
因此,BarIn
只能使用与声明的类型相同或更少的派生类型,因此它无法接收可能最终派生更多的IInterfaceIn
的T
。
然而,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
,意思是"将负数翻转为正数,将正数翻转为负数">方法。方法签名
:tmeth(t_1,...,t_n)
对于以下S
有效
- 其结果类型签名
t
对S
有效;和- 每个参数类型签名
t_i
对¬S
有效。- 每个方法泛型参数约束类型
t_j
对于¬S
都是有效的。[注意:换句话说,结果的行为是协变的,参数的行为是逆变的......
因此,我们翻转方法参数中使用的类型的方差。
所有这一切的结果是,在方法参数位置使用嵌套的协变或反变类型永远无效,因为所需的方差是翻转的,因此不会匹配。无论我们以哪种方式这样做,它都不会奏效。
相反,在返回位置使用委托始终有效。
我不确定这是否是协变与逆变问题。
Foo
委托不是接口的成员。它是一个嵌套类型声明。IInterface<A>.Foo
和IInterface<B>.Foo
是两种不同的类型。- 这使得两种不同
IInterface<T>.Baz
方法(T
=A
和B
)的foo
参数不兼容。 - 因此,您不能用
IInterface<A>
代替IInterface<B>
,反之亦然(无论A
和B
之间的继承关系如何。 - 结论:
IInterface<T>
不能是变异的(既不是co-,也不是contra-)。
分辨率:
- 将委托移动到顶层(在命名空间的正文中)。它是一个类型声明,因此不需要嵌入。
- 或者将其嵌入到没有类型参数的类型中。 例如,您可以为此创建一个非通用
IInterface
(并保留您的通用)。
但@EricLippert当然知道得更多。