使用接口的委托作为参数类型时,逆变无效

V0l*_*dek 9 c# generics covariance contravariance default-interface-member

考虑带有委托的逆变接口定义:

public interface IInterface<in TInput>
{
    delegate int Foo(int x);
    
    void Bar(TInput input);
    
    void Baz(TInput input, Foo foo);
}
Run Code Online (Sandbox Code Playgroud)

Baz失败的定义出现错误:

CS1961
无效方差:类型参数“ TInput”必须在“ IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)”上协变有效。' TInput' 是逆变的。

我的问题是为什么?乍一看,这应该是有效的,因为Foo委托与TInput. 我不知道是编译器过于保守还是我遗漏了什么。

请注意,通常您不会在接口内声明委托,特别是这不会在早于 C# 8 的版本上编译,因为接口中的委托需要默认接口​​实现。

如果允许这个定义,有没有办法打破类型系统,或者编译器是否保守?

Cha*_*ace 4

TL;博士; 根据 ECMA-335 规范,这是正确的,但令人困惑的是,在某些情况下它确实可以工作

\n

假设我们有两个变量

\n
IInterface<Animal> i1 = anInterfaceAnimalValue;\nIInterface<Cat>    i2 = anInterfaceCatValue;\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以拨打这些电话

\n
i1.Baz(anAnimal, j => 5);\n//this is the same as doing\ni1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));\n\ni1.Baz(aCat, j => 5);\n//this is the same as doing\ni1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));\n\n\ni2.Baz(aCat, j => 5);\n//this is the same as doing\ni2.Baz(aCat, new IInterface<Cat>.Foo(j => 5));\n
Run Code Online (Sandbox Code Playgroud)\n

如果我们现在分配,i1 = i2;会发生什么?

\n
i1.Baz(anAnimal, j => 5);\n//this is the same as doing\ni1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));\n
Run Code Online (Sandbox Code Playgroud)\n

但是IInterface<Cat>.Baz(实际的对象类型)不接受IInterface<Animal>.Foo,它只接受IInterface<Cat>.Foo这两个委托具有相同的签名这一事实并不意味着它们是不同的类型。

\n
\n

让我们更深入地探讨一下

\n

让我先说两点:

\n

首先,请记住,接口中的协变泛型类型可以出现在输出位置(这允许更多派生类型),而逆变则可以出现在输入位置(允许更多基本类型)。

\n
\n

泛型中的协变和逆变

\n

一般来说,协变类型参数可以用作委托的返回类型,而逆变类型参数可以用作参数类型。对于接口来说,协变类型参数可以作为接口方法的返回类型,逆变类型参数可以作为接口方法的参数类型。

\n
\n

对于传入参数的类型参数,这有点令人困惑:如果T是协变(输出),函数可以使用void (Action<T>)看起来像输入的函数,并且可以接受派生的委托。还可以返回Func<T>

\n

如果T是逆变,则相反。

\n

请参阅伟大的埃里克·利珀特 (Eric Lippert) 发表的这篇精彩文章以及彼得·杜尼霍 (Peter Duniho) 就同一问题发表的文章,以进一步解释这一点。

\n

其次,定义 CLI 规范的ECMA-335规定如下(我的粗体):

\n
\n

II.9.1 通用类型定义

\n

泛型参数在以下声明的范围内:

\n
    \n
  • 剪断...
  • \n
  • 除嵌套类之外的所有成员(实例和静态字段、方法、构造函数、属性和事件)。 [注意:C# 允许在嵌套类中使用封闭类中的泛型参数,但会将任何所需的额外泛型参数添加到元数据中的嵌套类定义中。尾注]
  • \n
\n
\n

因此,嵌套类型(其中Foo委托就是一个例子)实际上在范围内没有泛型T类型。C# 编译器将它们添加进来。

\n
\n

现在,看看下面的代码,我已经注意到哪些行不能编译:

\n
IInterface<Animal> i1 = anInterfaceAnimalValue;\nIInterface<Cat>    i2 = anInterfaceCatValue;\n
Run Code Online (Sandbox Code Playgroud)\n

让我们IInterfaceIn暂时坚持下去。

\n

采取无效BarIn。它使用FooIn,其类型参数是协变的。

\n

现在,如果我们有anAnimalInterfaceValue那么我们可以BarIn()FooIn<Animal>参数来调用。这意味着代表接受一个Animal论点。如果我们然后将其转换为IInterface<Cat>then ,我们可以使用 a 来调用它FooIn<Cat>,这需要一个类型为 的参数Cat,并且底层对象并不期望如此严格的委托,它期望能够传递any Animal

\n

因此只能使用与声明的BarIn类型相同或派生程度较低的类型,因此它无法接收可能最终派生更多的T类型。IInterfaceIn

\n

BarOut然而,它是有效的,因为它使用FooOut,它有一个相反的变体T

\n

现在让我们看看FooNestInFooNestOut。这些实际上重新声明了T封闭类型的参数。无效,因为它在输出位置FooNestOut使用协变体。虽然是有效的。in TFooNestIn

\n

让我们继续讨论BarNestBarNestInBarNestOut这些都是无效的,因为它们使用具有变通用参数的委托。这里的关键是我们不关心委托是否实际上在必要的位置使用了类型参数,我们关心的是委托的泛型参数的方差是否与我们提供的类型匹配。

\n

啊哈,你说,但是为什么IInterfaceOut嵌套参数不起作用呢?

\n

让我们再次看一下 ECMA-335,其中讨论了泛型参数有效,并断言泛型类型的每个部分都必须有效(我的粗体,S指的是泛型类型,例如List<T>T表示类型参数,var表示in/out各自参数的):

\n
\n

II.9.7 会员签名的有效性

\n

给定带注释的泛型参数S = <var_1 T_1, ..., var_n T_n>,我们定义类型定义的各个组件对于 而言有效的含义S。我们定义对注释的否定操作,写作\xc2\xacS,表示 \xe2\x80\x9cflip 负数到正数,正数到负数\xe2\x80\x9d

\n

方法。方法签名tmeth(t_1,...,t_n)对于S以下条件有效

\n
    \n
  • 其结果类型签名t对于以下方面有效S;和
  • \n
  • 每个参数类型签名t_i对于 而言都是有效的\xc2\xacS
  • \n
  • 每个方法通用参数约束类型t_j对于 而言都是有效的\xc2\xacS[注意:换句话说,结果表现为协变,而参数表现为逆变......
  • \n
\n
\n

因此,我们翻转方法参数中使用的类型的方差。

\n

所有这一切的结果是,在方法参数位置使用嵌套的协变逆变类型永远是无效的,因为所需的方差被翻转,因此不会匹配。无论我们采取哪种方式,都不会起作用。

\n

相反,在返回位置使用委托总是有效的。

\n