简体   繁体   English

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

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

The following raises complaints: 以下提出了投诉:

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

but I can't imagine where this wouldn't be type-safe. 但我无法想象这不会是类型安全的。 (snip*) Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of? (剪断*)这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?


* My initial thoughts were admittedly convoluted, but despite this, the responses are very thorough, and @Theodoros Chatzigiannakis even dissected my initial assumptions with impressive accuracy. *我最初的想法确实令人费解,但尽管如此,答案非常彻底, @ Theodoros Chatzigiannakis甚至以令人印象深刻的准确性解剖了我的初步假设。

Alongside a good slap from retrospect, I realize that I had falsely assumed that the type signature of ICovariant::M remains a Func<IInvariant<Derived>> when its ICovariant<Derived> is assigned to a ICovariant<Base> . 除了回顾过去的好评之外,我意识到当我的ICovariant<Derived>被分配给ICovariant<Base>时,我错误地认为ICovariant::M的类型签名仍然是Func<IInvariant<Derived>> Then, assigning that M to Func<IInvariant<Base>> would look fine coming from an ICovariant<Base> , but would of course be illegal. 然后,将M分配给Func<IInvariant<Base>> 看起来很好,来自ICovariant<Base> ,但当然是非法的。 Why not just ban this last, obviously-illegal cast? 为什么不禁止最后这个显然是非法的演员? (so I thought) (所以我认为)

I feel this false and tangential guess detracts from the question, as Eric Lippert also points out, but for historical purposes, the snipped part: 正如埃里克·利珀特Eric Lippert)所指出的那样,我觉得这种错误和切向猜测会减少这个问题,但出于历史目的,这个被剪切的部分:

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 . 对我来说最直观的解释是,以ICovariant为例,协变性TCov意味着方法IInvariant<TCov> M()可以被转换为某些IInvariant<TSuper> M() ,其中TSuper super TCov ,这违反了不变性IInvariantTInv However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M . 然而,这种暗示似乎并不是必要的:通过禁止M的演员,可以很容易地强制执行IInvariantTInv的不变性。

Let's look at a more concrete example. 让我们看一个更具体的例子。 We'll make a couple implementations of these interfaces: 我们将对这些接口进行一些实现:

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

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

Now, let's assume that the compiler didn't complain about this and try to use it in a simple way: 现在,让我们假设编译器没有抱怨这个并尝试以一种简单的方式使用它:

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

So far so good. 到现在为止还挺好。 o is ICovariant<object> and that interface guarantees that we have a method that can return an IInvariant<object> . oICovariant<object> ,该接口保证我们有一个可以返回IInvariant<object> We don't have to perform any casts or conversions here, everything is fine. 我们不必在这里进行任何演员表或转换,一切都很好。 Now let's call the method: 现在让我们调用方法:

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

Because ICovariant is covariant, this is a valid method call, we can substitute an ICovariant<string> wherever something wants an ICovariant<object> because of that covariance. 因为ICovariant是协变的,所以这是一个有效的方法调用,我们可以用任何想要ICovariant<object>ICovariant<string>替换因为该协方差。

But we have a problem. 但是我们遇到了问题。 Inside Foo , we call ICovariant<object>.M() and expect it to return an IInvariant<object> because that's what the ICovariant interface says it will do. Foo ,我们调用ICovariant<object>.M()并期望它返回一个IInvariant<object>因为这就是ICovariant接口所说的。 But it can't do that, because the actual implementation we've passed actually implements ICovariant<string> and its M method returns IInvariant<string> , which has nothing to do with IInvariant<object> due to the invariance of that interface. 但它不能这样做,因为我们传递的实际实现实际上实现了ICovariant<string>并且它的M方法返回IInvariant<string> ,由于该接口的不变性,它与IInvariant<object> 无关 They are completely different types. 它们是完全不同的类型。

I'm not sure that you actually got your question answered in either of the answers so far. 到目前为止,我不确定你的答案是否真的得到了答案。

Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters? 为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?

It doesn't, so the question is based on a false premise. 它没有,所以问题是基于错误的前提。 The actual rules are here: 实际规则如下:

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

Consider now: 现在考虑:

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? 这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?

I'm not following your explanation, so let's just say why this is disallowed without reference to your explanation. 我没有按照你的解释,所以我们只是说明为什么在不参考你的解释的情况下不允许这样做。 Here, let me replace these types with some equivalent types. 在这里,让我用一些等价的类型替换这些类型。 IInvariant<TInv> can be any type that is invariant in T, let's say ICage<TCage> : IInvariant<TInv>可以是T中不变的任何类型,假设ICage<TCage>

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

And maybe we have a type Cage<TAnimal> that implements ICage<TAnimal> . 也许我们有一个类型Cage<TAnimal>来实现ICage<TAnimal>

And let's replace ICovariant<T> with 让我们替换ICovariant<T>

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

Let's implement the interface: 让我们实现界面:

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

Everything is going so well. 一切都进展顺利。 ICageFactory is covariant, so this is legal: ICageFactory是协变的,所以这是合法的:

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

And we just put a fish into a tiger cage. 我们只是把鱼放进虎笼里。 Every step there was perfectly legal and we ended up with a type system violation. 那里的每一步都完全合法,我们最终违反了类型系统。 The conclusion we reach is that it must not have been legal to make ICageFactory covariant in the first place. 我们得出的结论是,首先使ICageFactory协变一定不合法。

Let's look at your contravariant example; 让我们来看看你的逆变例子; it's basically the same: 它基本相同:

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

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

And now, the interface is contravariant so this is legal: 现在,界面是逆变的,所以这是合法的:

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

Once again we have put a fish into a tiger cage. 我们又一次将鱼放入虎笼中。 Once again we conclude that it must have been illegal to make the type contravariant in the first place. 我们再次得出结论,首先制造类型逆变一定是非法的。

So now let's consider the question of how we know that these are illegal. 现在让我们考虑一下我们如何知道这些都是非法的问题。 In the first case we have 在第一种情况下我们有

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

And the relevant rule is: 相关规则是:

The return types of all non-void interface methods must be valid covariantly. 所有非void接口方法的返回类型必须是covariantly有效。

Is ICage<T> "valid covariantly"? ICage<T> “有效ICage<T> ”吗?

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! 如果类型是:1)指针类型或非泛型类,则该类型是有效的... NOPE 2)数组类型... NOPE 3)泛型类型参数类型... NOPE 4)构造类,struct,enum,interface或delegate type X<T1, … Tk> YES! ... If the ith type parameter was declared as invariant, then Ti must be valid invariantly. ...如果第i个类型参数被声明为不变量,那么Ti必须是不变的有效。

TAnimal was invariant in ICage<TAnimal> , So T in ICage<T> must be valid invariantly. TAnimal是不变ICage<TAnimal>所以TICage<T>必须是有效的不变地。 Is it? 是吗? No. To be valid invariantly it must be valid both covariantly and contravariantly, but it is valid only covariantly. 否。要有效目不暇接,它必须是有效的两个协变和contravariantly,但它是有效的只有协变。

Therefore this is an error. 因此这是一个错误。

Doing the analysis for the contravariant case is left as an exercise. 对逆变情况进行分析是一项练习。

Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters? 为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?

It doesn't! 它没有!

The return types and the argument types don't need to match the variance of the enclosing type. 返回类型和参数类型不需要与封闭类型的方差匹配。 In your example, they need to be covariant for both enclosing types. 在您的示例中,它们需要对两个封闭类型都是协变的。 It sounds counter-intuitive, but the reasons will become apparent in the explanation below. 这听起来违反直觉,但原因将在下面的解释中变得明显。


Why your proposed solution isn't valid 为什么您提出的解决方案无效

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 . 协变性TCov意味着方法IInvariant<TCov> M()可以转换为某些IInvariant<TSuper> M() ,其中TSuper super TCov ,它违反了IInvariantTInv的不变性。 However, this implication doesn't seem necessary: the invariance of IInvariant on TInv could easily be enforced by disallowing the cast of M . 然而,这种暗示似乎并不是必要的:通过禁止M的演员,可以很容易地强制执行IInvariantTInv的不变性。

  • What you're saying is that a generic type with a variant type parameter could be assigned to another type of the same generic type definition and a different type parameter. 您所说的是具有变体类型参数的泛型类型可以分配给同一泛型类型定义的另一种类型和不同的类型参数。 That part is correct. 那部分是正确的。
  • But you're also saying that in order to work around the potential subtyping violation problem, the apparent signature of the method should not change in the process. 但是你也说过,为了解决潜在的子类型违规问题,方法的明显签名不应该在这个过程中发生变化。 That's not correct! 那不对!

For example, ICovariant<string> has a method IInvariant<string> M() . 例如, ICovariant<string>有一个方法IInvariant<string> M() "Disallowing the cast of M " would mean that when ICovariant<string> is assigned to ICovariant<object> , it still retains the method with the signature IInvariant<string> M() . “禁止M ICovariant<string> ”意味着当ICovariant<string>被分配给ICovariant<object> ,它仍然保留带有签名IInvariant<string> M() If that were allowed, then this perfectly valid method would have a problem: 如果允许,那么这个完全有效的方法会有问题:

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

What type should the compiler infer for the type of the obj variable? 编译器应该为obj变量的类型推断出什么类型? Should it be IInvariant<string> ? 应该是IInvariant<string>吗? Why not IInvariant<Window> or IInvariant<UTF8Encoding> or IInvariant<TcpClient> ? 为什么不使用IInvariant<Window>IInvariant<UTF8Encoding>IInvariant<TcpClient> All of them can be valid, see for yourself: 所有这些都是有效的,请亲自看看:

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

Clearly, the statically known return type of a method ( M() ) can't possibly depend on an interface ( ICovariant<> ) implemented by the runtime type of the object! 显然, 静态已知的方法返回类型M() )不可能依赖于由对象运行时类型实现的接口( ICovariant<>

So when the generic type is assigned to another generic type with more general type arguments, the member signatures that use the corresponding type parameters must necessarily change to something more general as well. 因此,当泛型类型被分配给具有更多通用类型参数的另一个泛型类型时,使用相应类型参数的成员签名也必须更改为更通用的类型。 There's no way around it if we want to maintain type safety. 如果我们想要保持类型安全,就无法绕过它。 Now let's see what "more general" means in each case. 现在让我们看看在每种情况下“更一般”意味着什么。


Why ICovariant<TCov> requires IInvariant<TInv> to be covariant 为什么ICovariant<TCov>要求IInvariant<TInv>是协变的

For a type argument of string , the compiler "sees" this concrete type: 对于string的类型参数,编译器“看到”这个具体类型:

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

And (as we saw above) for a type argument of object , the compiler "sees" this concrete type instead: 并且(如上所述) object的类型参数,编译器“看到”这个具体类型:

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

Assume a type that implements the former interface: 假设实现前一个接口的类型:

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

Notice that the actual implementation of M() in this type is only concerned with returning an IInvariant<string> and it doesn't care about variance. 请注意, 此类型中M()的实际实现仅涉及返回IInvariant<string> ,而不关心方差。 Keep this in mind! 牢记这一点!

Now by making the type parameter of ICovariant<TCov> covariant, you are asserting that ICovariant<string> should be assignable to ICovariant<object> like this: 现在通过创建ICovariant<TCov>协变的类型参数,您断言ICovariant<string>应该可以像这样分配给ICovariant<object>

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

...and you are also asserting that you can now do this: ...而且你断言你现在可以这样做:

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

Remember, original.M() and covariant.M() are calls to the same method. 请记住, original.M()covariant.M()是对同一方法的调用。 And the actual method implementation only knows that it should return an Invariant<string> . 实际的方法实现只知道它应该返回一个Invariant<string>

So, at some point during the execution of the latter call, we are implicitly converting an IInvariant<string> (returned by the actual method) to an IInvariant<object> (which is what the covariant signature promises). 因此,在执行后一个调用的某个时刻,我们隐式地将IInvariant<string> (由实际方法返回)转换为IInvariant<object> (这是协变签名所承诺的)。 For this to happen, IInvariant<string> must be assignable to IInvariant<object> . 为此, IInvariant<string>必须可分配给IInvariant<object>

To generalize, the same relationship must apply for every IInvariant<S> and IInvariant<T> where S : T . 概括地说,相同的关系必须适用于每个IInvariant<S>IInvariant<T> ,其中S : T And that's exactly the description of a covariant type parameter. 这正是协变类型参数的描述。


Why IContravariant<TCon> also requires IInvariant<TInv> to be covariant 为什么IContravariant<TCon> 要求IInvariant<TInv>是协变的

For a type argument of object , the compiler "sees" this concrete type: 对于object的类型参数,编译器“看到”这个具体类型:

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

And for a type argument of string , the compiler "sees" this concrete type: 对于string的类型参数,编译器“看到”这个具体类型:

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

Assume a type that implements the former interface: 假设实现前一个接口的类型:

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

Again, notice that the actual implementation of M() assumes that it will get an IInvariant<object> from you and it doesn't care about variance. 再次注意, M()的实际实现假设它将从您那里获得一个IInvariant<object>并且它不关心方差。

Now by making the type parameter of IContravariant<TCon> , you are asserting that IContravariant<object> should be assignable to IContravariant<string> like this... 现在通过创建IContravariant<TCon>的类型参数,您断言IContravariant<object>应该像这样分配给IContravariant<string> ...

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

...and you are also asserting that you can now do this: ...而且你断言你现在可以这样做:

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

Again, original.M(arg) and contravariant.M(arg2) are calls to the same method. 同样, original.M(arg)contravariant.M(arg2)是对同一方法的调用。 The actual implementation of that method expects us to pass anything that's an IInvariant<object> . 该方法的实际实现要求我们传递任何IInvariant<object>

So, at some point during the execution of the latter call, we are implicitly converting an IInvariant<string> (which is what the contravariant signature expects from us) to an IInvariant<object> (which is what the actual method expects). 因此,在执行后一个调用期间的某个时刻,我们隐式地将IInvariant<string> (这是逆变量签名对我们的期望)转换为IInvariant<object> (这是实际方法所期望的)。 For this to happen, IInvariant<string> must be assignable to IInvariant<object> . 为此, IInvariant<string>必须可分配给IInvariant<object>

To generalize, every IInvariant<S> should be assignable to IInvariant<T> where S : T . 概括地说,每个IInvariant<S>应该可以赋予IInvariant<T> ,其中S : T So we're looking at a covariant type parameter again. 所以我们再次查看协变类型参数。


Now you may be wondering why there is a mismatch. 现在你可能想知道为什么会出现不匹配。 Where did the duality of covariance and contravariance go? 协方差和逆变的二元性在哪里? It's still there, but in a less obvious form: 它仍然存在,但形式不太明显:

  • When you are on the side of the outputs, the variance of the referenced type goes in the same direction as the variance of the enclosing type. 当您位于输出侧时,引用类型的方差与封闭类型的方差方向相同。 Since the enclosing type can be covariant or invariant in this case, the referenced type must also be covariant or invariant respectively. 由于封闭类型在这种情况下可以是协变的或不变的,因此引用的类型也必须分别是协变的或不变的。
  • When you are on the side of the inputs, the variance of the referenced type goes counter to the direction of the variance of the enclosing type. 当你在的输入侧,该被引用类型的方差变为逆着封闭类型的方差的方向。 Since the enclosing type can be contravariant or invariant in this case, the referenced type must now be covariant or invariant respectively. 由于封闭类型在这种情况下可以是逆变的或不变的,因此引用的类型现在必须分别是协变的或不变的。

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

相关问题 回报类型的差异 - Variance in return type 通用类型约束和方差 - Generic type constraint and variance 类型参数的C#variance注释,约束为值类型 - C# variance annotation of a type parameter, constrained to be value type 方差无效:type参数必须是无变量有效的,但是是协变的 - Invalid variance: The type parameter must be invariantly valid but is covariant 为什么条件方法的返回类型必须为void? - Why does conditional methods must have a return type of void? 为什么我会收到以下错误? 方差修饰符无效。 只能将接口和委托类型参数指定为变量 - Why do I get the following error? Invalid variance modifier. Only interface and delegate type parameters can be specified as variant 为什么具有返回类型的方法不能与返回void的具有相同签名的委托匹配? - Why can't methods that have a return type match a delegate of the same signature that returns void? 对于更高类型的类型,C#4.0中泛型类型参数的方差是否更接近? - Is variance of for generic type parameters in C# 4.0 a step closer for higher kind types? 如何解决这个错误? 方差无效:类型参数“T”必须是无效的 - How to fix this error? Invalid variance: The type parameter 'T' must be invariantly valid on 具有多个参数的Func方差 - Func variance with multiple parameters
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM