10

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

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 之前的版本上编译,因为接口中的委托需要默认接口​​实现。

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

4

2 回答 2

4

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

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

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

啊哈,你说,但是为什么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)对于Sif是有效的

  • 它的结果类型签名t对于S; 和
  • 每个参数类型签名t_i对于¬S.
  • 每个方法泛型参数约束类型t_j对于¬S. [注意:换句话说,结果的行为是协变的,而论点的行为是逆变的......

所以我们翻转了方法参数中使用的类型的方差

所有这一切的结果是,在方法参数位置使用嵌套的协变反变类型永远是无效的,因为所需的方差被翻转,因此不会匹配。不管我们怎么做,都行不通。

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

于 2021-02-27T19:36:43.480 回答
0

我不确定这是否是协方差问题。

  1. Foo委托不是接口的成员。它是一个嵌套类型声明。
  2. IInterface<A>.Foo并且IInterface<B>.Foo是两种不同的类型。
  3. 这使得foo两种不同IInterface<T>.Baz方法(使用T=AB)的参数不兼容。
  4. 因此,您不能将 a 替换IInterface<A>为 a IInterface<B>,反之亦然(无论Aand之间的继承关系如何B
  5. 结论:IInterface<T>不能是变体(既不共同也不反对)。

解析度:

  • 将委托移动到顶层(在命名空间的主体中)。它是一个类型声明,因此不需要嵌入。
  • 或者将其嵌入到没有类型参数的类型中。例如,您可以为此创建一个非泛型IInterface(并保留您的泛型)。

但@EricLippert 肯定知道得更好。

于 2021-03-01T14:46:03.217 回答