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

问题描述

考虑使用委托的逆变接口定义:

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;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,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 Lipperton 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,那么我们可以使用 BarIn() 参数调用 FooIn<Animal>。这意味着委托采用 Animal 参数。如果我们然后将其强制转换为 IInterface<Cat>,那么我们可以使用 FooIn<Cat> 调用它,它要求类型为 Cat 的参数,并且底层对象不期望如此严格的委托,它希望能够通过any Animal

因此 BarIn 只能使用与声明的类型相同或更少派生的类型,因此它无法接收 TIInterfaceIn最终可能会更多派生。

但是

BarOut 是有效的,因为它使用了 FooOut,它有一个 contra-variant T

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

让我们继续讨论 BarNestBarNestInBarNestOut。这些全部无效,因为它们使用具有 co-variant 通用参数的委托。 这里的关键是我们不关心委托是否真的在必要的位置使用了类型参数,我们关心的是委托的泛型参数的变化是否与我们提供的类型相匹配。强>

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

让我们再看看 ECMA-335,它谈到泛型参数有效,并断言泛型类型的每个部分都必须有效(我的粗体 S 指的是泛型类型,例如 {{1 }},List<T> 表示类型参数,T 表示相应参数的var):

II.9.7 成员签名的有效性

给定带注释的泛型参数 in/out,我们定义类型定义的各个组件对于 S = <var_1 T_1,...,var_n T_n> 有效的含义。我们在注解上定义了一个否定操作,写成 S,意思是“将负数翻转为正数,将正数翻转为负数”

方法。方法签名 ¬Stmeth(t_1,t_n) 有效

  • 其结果类型签名S对于t是有效的;和
  • 每个参数类型签名 S 对于 t_i 都是有效的。
  • 每个方法通用参数约束类型 ¬S 对于 t_j 都是有效的。 [注意:换句话说,结果是协变的,而参数是逆变的...

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

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

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

,

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

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

分辨率:

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

但@EricLippert 肯定知道得更好。