【问题标题】:Contravariance invalid when using interface's delegate as a parameter type使用接口的委托作为参数类型时逆变无效
【发布时间】:2021-09-24 20:25:43
【问题描述】:

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

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

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

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

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

请注意,通常您不会在接口中声明委托,尤其是在 C# 8 之前的版本上无法编译,因为接口中的委托需要默认接口​​实现。

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

【问题讨论】:

    标签: c# generics covariance contravariance default-interface-member


    【解决方案1】:

    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&lt;Cat&gt;.Baz(实际的对象类型)不接受IInterface&lt;Animal&gt;.Foo,它只接受IInterface&lt;Cat&gt;.Foo这两个委托是相同的签名这一事实并没有从他们身上带走是不同的类型。


    让我们再深入一点

    让我先说两点:

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

    Covariance and contravariance in generics

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

    使用您传入的参数的类型参数,这有点令人困惑:如果T 是协变的(输出),则函数可以使用void (Action&lt;T&gt;),它看起来像是一个输入,并且可以接受更多派生的委托。也可以返回Func&lt;T&gt;

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

    因此BarIn 只能使用与声明的类型相同或 派生的类型,因此它无法接收IInterfaceInT,它可能以更多衍生。

    但是,

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

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

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

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

    让我们再看一下 ECMA-335,它谈到泛型参数是有效的,并断言泛型类型的每个部分都必须是有效的(我的粗体,S 指的是泛型类型,例如 List&lt;T&gt;,@ 987654368@ 表示类型参数,var 表示相应参数的in/out):

    II.9.7 会员签名的有效性

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

    方法。方法签名tmeth(t_1,...,t_n) 相对于S 有效

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

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

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

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

    【讨论】:

    • 你可以忽略它。如果没有实际的声明,我认为它们是接口类型,而不是委托。另外,我还不够清醒,不能权威地说出这个,但我认为您对委托类型所做的区别实际上是存在于 any 泛型类型的区别在类型变体场景中使用变体参数时。 IE。反转 co/contra 方面的事情是通过参数或返回值的额外间接级别,而不是它本身是委托或接口。 (直觉上这对我来说很有意义,因为代表真的......
    • ...只是语法更简单的单成员接口。)(叹息...刚刚结束再次
    • 找到了 Eric L 的链接,编辑哦,你也写了一些东西。我认为你是对的。除了他们是单身成员.... :-)
    • 是的,几乎所有 Lippert 关于该主题的文章都将成为黄金标准。他总是知道这些东西,但更重要的是,在 99.94% 的情况下,他在表达事实的方式上有着独特的清晰性,这使得它比其他人写的更容易理解。 (不确定你反对我对代表的描述......我将它们与接口进行了比较,或者它们等同于具有 more 一个成员的接口?请注意我不'并不意味着它们与运行时相同......只是在语义上它们是等价的。)
    • 他们不止一个成员。参见 ECMA-335 II.14.6.3,显然还有一个反编译器。诚然,ECMA 并不需要异步方法,但 MS 的 CLI 实现确实这样做了。
    【解决方案2】:

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

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

    分辨率:

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

    但@EricLippert 肯定知道得更好。

    【讨论】:

    • 我希望我的文字中的@EricLippert 会打电话回家:-)
    • 鉴于你的结论,这似乎一个方差问题,所以我认为你应该删除你的第一句话。
    • @Servy 好吧,我稍微修改了一下我的句子。
    • 如果不是方差问题(意味着IInterface&lt;Cat&gt;.Foo 永远不会与IInterface&lt;Animal&gt;.Foo 分配兼容,那么您如何解释我给出的FooNest BazNest() 示例,确实 编译?顺便说一句,不知道你的反对意见是什么,我说错了吗?
    • @Charlieface:我没有投反对票(实际上我昨天投了赞成票)。也许你是对的。我的感觉是嵌套类型不会参与周围类型的变化。
    猜你喜欢
    • 2011-03-25
    • 1970-01-01
    • 2021-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多