11

以下引起投诉:

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()'
}

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


* 诚然,我最初的想法令人费解,但尽管如此,反应非常彻底,@Theodoros Chatzigiannakis甚至以令人印象深刻的准确度剖析了我最初的假设。

回想起来,除了一个很好的耳光外,我意识到我错误地假设当它被分配给ICovariant::Ma 时,它的类型签名仍然是 a 。然后,将其分配给来自 看起来不错,但当然是非法的。为什么不直接禁止最后一个明显非法的演员阵容?(所以我认为)Func<IInvariant<Derived>>ICovariant<Derived>ICovariant<Base>MFunc<IInvariant<Base>>ICovariant<Base>

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

对我来说最直观的解释是,以ICovariant协变为例,TCov意味着该方法IInvariant<TCov> M()可以强制转换为某个IInvariant<TSuper> M()where TSuper super TCov,这违反了TInvin的不变性IInvariant。然而,这种暗示似乎没有必要:通过IInvariant禁止.TInvM

4

3 回答 3

6

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

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();
}

到现在为止还挺好。 oisICovariant<object>并且该接口保证我们有一个可以返回IInvariant<object>. 我们不必在这里执行任何强制转换或转换,一切都很好。现在让我们调用该方法:

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

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

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

于 2016-05-17T15:56:21.397 回答
5

到目前为止,我不确定您是否真的在任何一个答案中都回答了您的问题。

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

它没有,所以这个问题是基于一个错误的前提。实际规则在这里:

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 接口方法的返回类型必须协变有效。

ICage<T>有效协变”?

一个类型是协变有效的,如果它是: 1) 指针类型,或非泛型类... NOPE 2) 数组类型... NOPE 3) 泛型类型参数类型... NOPE 4) 构造类、结构、枚举、接口或委托类型X<T1, … Tk>YES!...如果第 i 个类型参数被声明为不变,那么 Ti 必须不变有效。

TAnimal在 中是不变的ICage<TAnimal>,所以TinICage<T>必须是不变的。是吗?不。要保持不变有效,它必须协变和逆变有效,但它只能协变有效。

因此这是一个错误。

对逆变情况进行分析留作练习。

于 2016-05-17T20:07:15.573 回答
1

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

它没有!

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


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

协变TCov意味着该方法IInvariant<TCov> M()可以强制转换到某个IInvariant<TSuper> M()where TSuper super TCov,这违反了TInvin的不变性IInvariant。然而,这种暗示似乎没有必要:通过IInvariant禁止.TInvM

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

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

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

obj编译器应该为变量的类型推断什么类型?应该是IInvariant<string>吗?为什么不IInvariant<Window>orIInvariant<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>.

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

概括地说,相同的关系必须适用于 everyIInvariant<S>IInvariant<T>where 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>.

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

概括地说,everyIInvariant<S>应该可以分配给IInvariant<T>where S : T。所以我们再次查看协变类型参数。


现在您可能想知道为什么会出现不匹配。协变和逆变的对偶性哪里去了?它仍然存在,但形式不太明显:

  • 当您站在输出端时,引用类型的方差与封闭类型的方差方向相同。由于在这种情况下封闭类型可以是协变或不变的,因此被引用的类型也必须分别是协变或不变的。
  • 当您站在输入一边时,引用类型的方差与封闭类型的方差方向相反由于在这种情况下封闭类型可以是逆变的或不变的,所以引用的类型现在必须分别是协变的或不变的。
于 2016-05-17T16:33:58.007 回答