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

Contravariance invalid when using interface's delegate as a parameter type

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

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

Baz 的定义失败并出现错误:

CS1961
Invalid variance: The type parameter 'TInput' must be covariantly valid on 'IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)'. 'TInput' is contravariant.

我的问题是为什么?乍一看这应该是有效的,因为 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, 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.这两个事实代表具有相同的签名并不会影响他们成为不同的类型。


让我们再深入一点

先说两点:

首先,记住接口中的协变泛型类型可以出现在output位置(这允许更派生的类型), input 位置的反变体(允许更多的基本类型)。

Covariance and contravariance in generics

In general, a covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types. For an interface, covariant type parameters can be used as the return types of the interface's methods, and contravariant type parameters can be used as the parameter types of the interface's methods.

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

如果 T 是逆变的,则相反。

有关这一点的进一步说明,请参阅 and

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

II.9.1 Generic type definitions

The generic parameter is in scope in the declarations of:

  • snip...
  • all members (instance and static fields, methods, constructors, properties and events) except nested classes. [Note: C# allows generic parameters from an enclosing class to be used in a nested class, but adds any required extra generic parameters to the nested class definition in metadata. 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);     //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 的参数和底层对象不期待这么严格的委托,它期望能够通过 any Animal.

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

然而,

BarOut 是有效的,因为它使用 FooOut,它有一个 contra-变体 T.

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

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

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

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

II.9.7 Validity of member signatures

Given the annotated generic parameters S = <var_1 T_1, ..., var_n T_n>, we define what it means for various components of the type definition to be valid with respect to S. We define a negation operation on annotations, written ¬S, to mean “flip negatives to positives, and positives to negatives”

Methods. A method signature tmeth(t_1,...,t_n) is valid with respect to S if

  • its result type signature t is valid with respect to S; and
  • each argument type signature t_i is valid with respect to ¬S.
  • each method generic parameter constraint type t_j is valid with respect to ¬S. [Note: In other words, the result behaves covariantly and the arguments behave contravariantly...

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

所有这一切的结果是 永远不会 有效地在方法中使用嵌套的联合 or contra-variant 类型参数位置,因为所需的 variance 被翻转,因此不会匹配。不管我们怎么做,都行不通。

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

我不确定这是否是协变与逆变问题。

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

分辨率:

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

但@EricLippert 肯定知道得更多。