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

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

但我无法想象这在什么地方不是类型安全的。 (snip*) 这是不允许这样做的原因,还是存在其他我不知道的违反类型安全的情况?


* 诚然,我最初的想法是令人费解的,但尽管如此,回复还是非常彻底的, 甚至以令人印象深刻的准确性剖析了我最初的假设。

除了回想起来的一个好耳光,我意识到我错误地认为 ICovariant::M 的类型签名在其 ICovariant<Derived> 分配给 [=16 时仍然是 Func<IInvariant<Derived>> =].然后,将 M 分配给 Func<IInvariant<Base>> 看起来 来自 ICovariant<Base> 很好,但当然是非法的。为什么不直接禁止这最后一个明显非法的演员表呢? (所以我想)

我觉得这个错误的和切线的猜测有损于问题,正如 也指出的那样,但出于历史目的,被删减的部分:

The most intuitive explanation to me is that, taking ICovariant as an example, the covariant TCov implies that the method IInvariant<TCov> M() could be cast to some IInvariant<TSuper> M() where TSuper super TCov, which violates the invariance of TInv in IInvariant. However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M.

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

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> 并且该接口保证我们有一个可以 return 和 IInvariant<object> 的方法。我们不必在这里执行任何转换或转换,一切都很好。现在让我们调用方法:

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

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

但是我们有一个问题。在 Foo 中,我们调用 ICovariant<object>.M() 并期望它成为 return 和 IInvariant<object>,因为这就是 ICovariant 接口所说的它将执行的操作。但它不能那样做,因为我们传递的实际实现实际上实现了 ICovariant<string> 及其 M 方法 returns IInvariant<string>,其中有 由于该接口的不变性,IInvariant<object> 无关。他们是完全不同的类型。

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

没有!

return 类型和参数类型不需要匹配封闭类型的变体。在您的示例中,对于两种封闭类型,它们都需要 covariant。这听起来有悖常理,但原因会在下面的解释中变得显而易见。


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

the covariant TCov implies that the method IInvariant<TCov> M() could be cast to some IInvariant<TSuper> M() where TSuper super TCov, which violates the invariance of TInv in IInvariant. However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M.

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

例如,ICovariant<string> 有一个方法 IInvariant<string> M()。 "Disallowing the cast of M" 意味着当 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()) 的 静态已知 return 类型 不可能依赖于接口 (ICovariant<> ) 由对象的运行时类型实现!

因此,当将泛型类型分配给具有更通用类型参数的另一个泛型类型时,使用相应类型参数的成员签名也必须更改为更通用的类型。如果我们想保持类型安全,就没有办法绕过它。现在让我们看看 "more general" 在每种情况下的含义。


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

对于 string 的类型参数,编译器 "sees" 这个具体类型:

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

并且(正如我们上面看到的)对于 object 的类型参数,编译器 "sees" 这个具体类型改为:

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

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

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

请注意,此类型中 M() 的实际实现仅与 returning 一个 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() 是对同一方法的调用。而实际的方法实现只知道它应该 return 一个 Invariant<string>.

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

总而言之,相同的关系必须适用于每个 IInvariant<S>IInvariant<T>,其中 S : T。这正是对协变类型参数的描述。


为什么IContravariant<TCon> 需要IInvariant<TInv>协变

对于 object 的类型参数,编译器 "sees" 这个具体类型:

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

并且对于 string 的类型参数,编译器 "sees" 这个具体类型:

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。所以我们再次查看协变类型参数。


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

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

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

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

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

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
}

Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?

我没有听懂你的解释,所以我们只说为什么这是不允许的,而不参考你的解释。在这里,让我用一些等效的类型替换这些类型。 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();
}

相关规则为:

The return types of all non-void interface methods must be valid covariantly.

ICage<T>"valid covariantly"吗?

A type is valid covariantly if it is: 1) a pointer type, or a non-generic class... NOPE 2) An array type... NOPE 3) A generic type parameter type ... NOPE 4) A constructed class, struct, enum, interface or delegate type X<T1, … Tk> YES! ... If the ith type parameter was declared as invariant, then Ti must be valid invariantly.

TAnimalICage<TAnimal>中是不变的,所以ICage<T>中的T一定是不变的。是吗?不。要始终有效,它必须协变和逆变都有效,但它仅协变有效。

因此这是一个错误。

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