繁体   English   中英

为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?

[英]Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?

以下提出了投诉:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

但我无法想象这不会是类型安全的。 (剪断*)这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?


*我最初的想法确实令人费解,但尽管如此,答案非常彻底, @ Theodoros Chatzigiannakis甚至以令人印象深刻的准确性解剖了我的初步假设。

除了回顾过去的好评之外,我意识到当我的ICovariant<Derived>被分配给ICovariant<Base>时,我错误地认为ICovariant::M的类型签名仍然是Func<IInvariant<Derived>> 然后,将M分配给Func<IInvariant<Base>> 看起来很好,来自ICovariant<Base> ,但当然是非法的。 为什么不禁止最后这个显然是非法的演员? (所以我认为)

正如埃里克·利珀特Eric Lippert)所指出的那样,我觉得这种错误和切向猜测会减少这个问题,但出于历史目的,这个被剪切的部分:

对我来说最直观的解释是,以ICovariant为例,协变性TCov意味着方法IInvariant<TCov> M()可以被转换为某些IInvariant<TSuper> M() ,其中TSuper super TCov ,这违反了不变性IInvariantTInv 然而,这种暗示似乎并不是必要的:通过禁止M的演员,可以很容易地强制执行IInvariantTInv的不变性。

让我们看一个更具体的例子。 我们将对这些接口进行一些实现:

class InvariantImpl<T> : IInvariant<T>
{
}

class CovariantImpl<T> : ICovariant<T>
{
    public IInvariant<T> M()
    {
        return new InvariantImpl<T>();
    }
}

现在,让我们假设编译器没有抱怨这个并尝试以一种简单的方式使用它:

static IInvariant<object> Foo( ICovariant<object> o )
{
    return o.M();
}

到现在为止还挺好。 oICovariant<object> ,该接口保证我们有一个可以返回IInvariant<object> 我们不必在这里进行任何演员表或转换,一切都很好。 现在让我们调用方法:

var x = Foo( new CovariantImpl<string>() );

因为ICovariant是协变的,所以这是一个有效的方法调用,我们可以用任何想要ICovariant<object>ICovariant<string>替换因为该协方差。

但是我们遇到了问题。 Foo ,我们调用ICovariant<object>.M()并期望它返回一个IInvariant<object>因为这就是ICovariant接口所说的。 但它不能这样做,因为我们传递的实际实现实际上实现了ICovariant<string>并且它的M方法返回IInvariant<string> ,由于该接口的不变性,它与IInvariant<object> 无关 它们是完全不同的类型。

到目前为止,我不确定你的答案是否真的得到了答案。

为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?

它没有,所以问题是基于错误的前提。 实际规则如下:

https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

现在考虑:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
   IInvariant<TCov> M(); // Error
}

这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?

我没有按照你的解释,所以我们只是说明为什么在不参考你的解释的情况下不允许这样做。 在这里,让我用一些等价的类型替换这些类型。 IInvariant<TInv>可以是T中不变的任何类型,假设ICage<TCage>

interface ICage<TAnimal> {
  TAnimal Remove();
  void Insert(TAnimal contents);
}

也许我们有一个类型Cage<TAnimal>来实现ICage<TAnimal>

让我们替换ICovariant<T>

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

让我们实现界面:

class TigerCageFactory : ICageFactory<Tiger> 
{ 
  public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
}

一切都进展顺利。 ICageFactory是协变的,所以这是合法的:

ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
ICage<Animal> animalCage = animalCageFactory.MakeCage();
animalCage.Insert(new Fish());

我们只是把鱼放进虎笼里。 那里的每一步都完全合法,我们最终违反了类型系统。 我们得出的结论是,首先使ICageFactory协变一定不合法。

让我们来看看你的逆变例子; 它基本相同:

interface ICageFiller<in T> {
   void Fill(ICage<T> cage);
}

class AnimalCageFiller : ICageFiller<Animal> {
  public void Fill(ICage<Animal> cage)
  {
    cage.Insert(new Fish());
  }
}

现在,界面是逆变的,所以这是合法的:

ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
tigerCageFiller.Fill(new Cage<Tiger>());

我们又一次将鱼放入虎笼中。 我们再次得出结论,首先制造类型逆变一定是非法的。

现在让我们考虑一下我们如何知道这些都是非法的问题。 在第一种情况下我们有

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

相关规则是:

所有非void接口方法的返回类型必须是covariantly有效。

ICage<T> “有效ICage<T> ”吗?

如果类型是:1)指针类型或非泛型类,则该类型是有效的... NOPE 2)数组类型... NOPE 3)泛型类型参数类型... NOPE 4)构造类,struct,enum,interface或delegate type X<T1, … Tk> YES! ...如果第i个类型参数被声明为不变量,那么Ti必须是不变的有效。

TAnimal是不变ICage<TAnimal>所以TICage<T>必须是有效的不变地。 是吗? 否。要有效目不暇接,它必须是有效的两个协变和contravariantly,但它是有效的只有协变。

因此这是一个错误。

对逆变情况进行分析是一项练习。

为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?

它没有!

返回类型和参数类型不需要与封闭类型的方差匹配。 在您的示例中,它们需要对两个封闭类型都是协变的。 这听起来违反直觉,但原因将在下面的解释中变得明显。


为什么您提出的解决方案无效

协变性TCov意味着方法IInvariant<TCov> M()可以转换为某些IInvariant<TSuper> M() ,其中TSuper super TCov ,它违反了IInvariantTInv的不变性。 然而,这种暗示似乎并不是必要的:通过禁止M的演员,可以很容易地强制执行IInvariantTInv的不变性。

  • 您所说的是具有变体类型参数的泛型类型可以分配给同一泛型类型定义的另一种类型和不同的类型参数。 那部分是正确的。
  • 但是你也说过,为了解决潜在的子类型违规问题,方法的明显签名不应该在这个过程中发生变化。 那不对!

例如, ICovariant<string>有一个方法IInvariant<string> M() “禁止M ICovariant<string> ”意味着当ICovariant<string>被分配给ICovariant<object> ,它仍然保留带有签名IInvariant<string> M() 如果允许,那么这个完全有效的方法会有问题:

void Test(ICovariant<object> arg)
{
    var obj = arg.M();
}

编译器应该为obj变量的类型推断出什么类型? 应该是IInvariant<string>吗? 为什么不使用IInvariant<Window>IInvariant<UTF8Encoding>IInvariant<TcpClient> 所有这些都是有效的,请亲自看看:

Test(new CovariantImpl<string>());
Test(new CovariantImpl<Window>());
Test(new CovariantImpl<UTF8Encoding>());
Test(new CovariantImpl<TcpClient>());

显然, 静态已知的方法返回类型M() )不可能依赖于由对象运行时类型实现的接口( ICovariant<>

因此,当泛型类型被分配给具有更多通用类型参数的另一个泛型类型时,使用相应类型参数的成员签名也必须更改为更通用的类型。 如果我们想要保持类型安全,就无法绕过它。 现在让我们看看在每种情况下“更一般”意味着什么。


为什么ICovariant<TCov>要求IInvariant<TInv>是协变的

对于string的类型参数,编译器“看到”这个具体类型:

interface ICovariant<string>
{
    IInvariant<string> M();
}

并且(如上所述) object的类型参数,编译器“看到”这个具体类型:

interface ICovariant<object>
{
    IInvariant<object> M();
}

假设实现前一个接口的类型:

class MyType : ICovariant<string>
{
    public IInvariant<string> M() 
    { /* ... */ }
}

请注意, 此类型中M()的实际实现仅涉及返回IInvariant<string> ,而不关心方差。 牢记这一点!

现在通过创建ICovariant<TCov>协变的类型参数,您断言ICovariant<string>应该可以像这样分配给ICovariant<object>

ICovariant<string> original = new MyType();
ICovariant<object> covariant = original;

...而且你断言你现在可以这样做:

IInvariant<string> r1 = original.M();
IInvariant<object> r2 = covariant.M();

请记住, original.M()covariant.M()是对同一方法的调用。 实际的方法实现只知道它应该返回一个Invariant<string>

因此,在执行后一个调用的某个时刻,我们隐式地将IInvariant<string> (由实际方法返回)转换为IInvariant<object> (这是协变签名所承诺的)。 为此, IInvariant<string>必须可分配给IInvariant<object>

概括地说,相同的关系必须适用于每个IInvariant<S>IInvariant<T> ,其中S : T 这正是协变类型参数的描述。


为什么IContravariant<TCon> 要求IInvariant<TInv>是协变的

对于object的类型参数,编译器“看到”这个具体类型:

interface IContravariant<object>
{
    void M(IInvariant<object> v); 
}

对于string的类型参数,编译器“看到”这个具体类型:

interface IContravariant<string>
{
    void M(IInvariant<string> v); 
}

假设实现前一个接口的类型:

class MyType : IContravariant<object>
{
    public void M(IInvariant<object> v)
    { /* ... */ }
}

再次注意, M()的实际实现假设它将从您那里获得一个IInvariant<object>并且它不关心方差。

现在通过创建IContravariant<TCon>的类型参数,您断言IContravariant<object>应该像这样分配给IContravariant<string> ...

IContravariant<object> original = new MyType();
IContravariant<string> contravariant = original;

...而且你断言你现在可以这样做:

IInvariant<object> arg = Something();
original.M(arg);
IInvariant<string> arg2 = SomethingElse();
contravariant.M(arg2);

同样, original.M(arg)contravariant.M(arg2)是对同一方法的调用。 该方法的实际实现要求我们传递任何IInvariant<object>

因此,在执行后一个调用期间的某个时刻,我们隐式地将IInvariant<string> (这是逆变量签名对我们的期望)转换为IInvariant<object> (这是实际方法所期望的)。 为此, IInvariant<string>必须可分配给IInvariant<object>

概括地说,每个IInvariant<S>应该可以赋予IInvariant<T> ,其中S : T 所以我们再次查看协变类型参数。


现在你可能想知道为什么会出现不匹配。 协方差和逆变的二元性在哪里? 它仍然存在,但形式不太明显:

  • 当您位于输出侧时,引用类型的方差与封闭类型的方差方向相同。 由于封闭类型在这种情况下可以是协变的或不变的,因此引用的类型也必须分别是协变的或不变的。
  • 当你在的输入侧,该被引用类型的方差变为逆着封闭类型的方差的方向。 由于封闭类型在这种情况下可以是逆变的或不变的,因此引用的类型现在必须分别是协变的或不变的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM