没有警告或错误(或运行时故障)时,逆变导致歧义 [英] No warning or error (or runtime failure) when contravariance leads to ambiguity

查看:164
本文介绍了没有警告或错误(或运行时故障)时,逆变导致歧义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先,请记住,一个.NET 字符串既是 IConvertible ICloneable

First, remember that a .NET String is both IConvertible and ICloneable.

现在,考虑下面很简单code:

Now, consider the following quite simple code:

//contravariance "in"
interface ICanEat<in T> where T : class
{
  void Eat(T food);
}

class HungryWolf : ICanEat<ICloneable>, ICanEat<IConvertible>
{
  public void Eat(IConvertible convertibleFood)
  {
    Console.WriteLine("This wolf ate your CONVERTIBLE object!");
  }

  public void Eat(ICloneable cloneableFood)
  {
    Console.WriteLine("This wolf ate your CLONEABLE object!");
  }
}

请尝试以下的(内部的一些方法):

Then try the following (inside some method):

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

当编译这一点,得到的没有的编译器错误或警告。当运行它,它看起来像调用的方法在我的声明取决于接口列表的顺序为 HungryWolf 。 (尝试交换两个接口的逗号()分隔的列表。)

When one compiles this, one gets no compiler error or warning. When running it, it looks like the method called depends on the order of the interface list in my class declaration for HungryWolf. (Try swapping the two interfaces in the comma (,) separated list.)

现在的问题很简单:?不应该这样给一个编译时警告(或扔在运行时)

The question is simple: Shouldn't this give a compile-time warning (or throw at run-time)?

我可能不是第一个拿出code这样。我用的接口的逆变的,但你可以让一个完全类似的接口例如用的 covarainace 的。而事实上,<一个href="http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx">Mr利珀特就这么做了一个很久以前。在他的博客的评论,几乎所有人都认为它应该是一个错误。然而,他们允许这样悄无声息。 为什么?

I'm probably not the first one to come up with code like this. I used contravariance of the interface, but you can make an entirely analogous example with covarainace of the interface. And in fact Mr Lippert did just that a long time ago. In the comments in his blog, almost everyone agrees that it should be an error. Yet they allow this silently. Why?

---

扩展问题:

以上我们开发了一个字符串既是 Iconvertible (接口)和 ICloneable (接口)。无论是这两个接口的派生自其他。

Above we exploited that a String is both Iconvertible (interface) and ICloneable (interface). Neither of these two interfaces derives from the other.

现在这里是与基类的一个例子是,在某种意义上,有点差。

Now here's an example with base classes that is, in a sense, a bit worse.

请记住, StackOverflowException 既是 SystemException的(直接基类)和异常(基类的基类)。然后,(如果 ICanEat&LT;&GT; 就像前):

Remember that a StackOverflowException is both a SystemException (direct base class) and an Exception (base class of base class). Then (if ICanEat<> is like before):

class Wolf2 : ICanEat<Exception>, ICanEat<SystemException>  // also try reversing the interface order here
{
  public void Eat(SystemException systemExceptionFood)
  {
    Console.WriteLine("This wolf ate your SYSTEM EXCEPTION object!");
  }

  public void Eat(Exception exceptionFood)
  {
    Console.WriteLine("This wolf ate your EXCEPTION object!");
  }
}

与测试一下:

Test it with:

static void Main()
{
  var w2 = new Wolf2();
  w2.Eat(new StackOverflowException());          // OK, one overload is more "specific" than the other

  ICanEat<StackOverflowException> w2Soe = w2;    // Contravariance
  w2Soe.Eat(new StackOverflowException());       // Depends on interface order in Wolf2
}

仍然没有警告,错误或异常。仍取决于接口列表以便在声明。但是,为什么我认为这是糟糕的原因是,这时候有人可能会认为,重载总是挑 SystemException的,因为它不仅仅是异常

Still no warning, error or exception. Still depends on interface list order in class declaration. But the reason why I think it's worse is that this time someone might think that overload resolution would always pick SystemException because it's more specific than just Exception.

赏金被打开之前状态:三个答案来自两个用户

Status before the bounty was opened: Three answers from two users.

在赏金的最后一天状态:仍然没有新的答案好评。如果没有答案显示出来,我得奖奖金为穆斯林奔Dhaou。

Status on the last day of the bounty: Still no new answers received. If no answers show up, I shall have to award the bounty to Moslem Ben Dhaou.

推荐答案

我相信编译器在VB.NET中更愉快的前车之鉴,但我仍然不认为这会远远不够。不幸的是,正确的事可能要么要求不允许的东西是潜在有用的(实现相同的接口有两个协变或逆变泛型类型参数),或引入一些新的语言。

I believe the compiler does the better thing in VB.NET with the warning, but I still don't think that is going far enough. Unfortunately, the "right thing" probably either requires disallowing something that is potentially useful(implementing the same interface with two covariant or contravariant generic type parameters) or introducing something new to the language.

因为它的立场,没有任何地方的编译器可以指定一个错误,现在以外的 HungryWolf 类。这是点上,一类是自称知道如何做一些事情,可能是模糊的。它说明

As it stands, there is no place the compiler could assign an error right now other than the HungryWolf class. That is the point at which a class is claiming to know how to do something that could potentially be ambiguous. It is stating

我知道如何吃一个 ICloneable ,或任何实施或从它继承,以一定的方式。

I know how to eat an ICloneable, or anything implementing or inheriting from it, in a certain way.

和,我也知道怎么吃的 IConvertible ,或任何实施或从它继承,以一定的方式。

And, I also know how to eat an IConvertible, or anything implementing or inheriting from it, in a certain way.

不过,它从来没有规定它应该做如果收到其板的东西是既是 ICloneable IConvertible 。这不会导致编译器的任何悲伤,如果它被赋予 HungryWolf 的实例,因为它可以肯定地说的嘿,我不知道该怎么在这里做的!的。但它会给编译器悲痛它被赋予在 ICanEat&LT;字符串&GT; 实例。 编译器不知道是什么物体在变量的实际类型,只知道它肯定不会实施 ICanEat&LT;字符串&GT;

However, it never states what it should do if it receives on its plate something that is both an ICloneable and an IConvertible. This doesn't cause the compiler any grief if it is given an instance of HungryWolf, since it can say with certainty "Hey, I don't know what to do here!". But it will give the compiler grief when it is given the ICanEat<string> instance. The compiler has no idea what the actual type of the object in the variable is, only that it definitely does implement ICanEat<string>.

不幸的是,当 HungryWolf 存储在一个变量,它模棱两可实现了完全一样的接口两次。因此可以肯定,我们不能抛出异常试图调用 ICanEat&LT;字符串&GT;。吃(字符串),因为该方法存在,将是非常有效的可能是许多其他对象放入 ICanEat&LT;字符串&GT; 变量( batwad 已经提到了这一点在他的回答中的一个)。

Unfortunately, when a HungryWolf is stored in that variable, it ambiguously implements that exact same interface twice. So surely, we cannot throw an error trying to call ICanEat<string>.Eat(string), as that method exists and would be perfectly valid for many other objects which could be placed into the ICanEat<string> variable (batwad already mentioned this in one of his answers).

此外,尽管编译器可能会抱怨说,一个 HungryWolf 对象的 ICanEat 1所述的转让;串&GT; 变量是不明确的,它不能prevent它发生在两个步骤。 A HungryWolf 可以被分配到一个 ICanEat&LT; IConvertible&GT; 变量,这可能是各地传递到其他的方法,并最终分配到 ICanEat&LT;字符串&GT; 变量。 这两个是完全合法的分配,这将是不可能的编译器抱怨任何一个。

Further, although the compiler could complain that the assignment of a HungryWolf object to an ICanEat<string> variable is ambiguous, it cannot prevent it from happening in two steps. A HungryWolf can be assigned to an ICanEat<IConvertible> variable, which could be passed around into other methods and eventually assigned into an ICanEat<string> variable. Both of these are perfectly legal assignments and it would be impossible for the compiler to complain about either one.

因此​​,选项一个是从同时实现 ICanEat&LT禁止的 HungryWolf 类; IConvertible&GT; ICanEat&LT; ICloneable&GT; ICanEat 的泛型类型参数是逆变的,因为这两个接口可以统一。然而,这种删除的能力,code一些有用的东西有没有替代的解决方法。

Thus, option one is to disallow the HungryWolf class from implementing both ICanEat<IConvertible> and ICanEat<ICloneable> when ICanEat's generic type parameter is contravariant, since these two interfaces could unify. However, this removes the ability to code something useful with no alternative workaround.

选择二,遗憾的是,将需要改变编译器,无论是IL和CLR。这将使 HungryWolf 类来实现这两个接口,但它也需要接口的实现 ICanEat&LT; IConvertible&安培; ICloneable&GT; 界面,在这里泛型类型参数实现这两个接口。这可能不是最好的语法(什么呢这个签名吃(T)法的样子,吃(IConvertible&安培; ICloneable食品)?)。可能的是,一个更好的解决办法是自动生成的泛型类型的实现类后,使类的定义是这样的:

Option two, unfortunately, would require a change to the compiler, both the IL and the CLR. It would allow the HungryWolf class to implement both interfaces, but it would also require the implementation of the interface ICanEat<IConvertible & ICloneable> interface, where the generic type parameter implements both interfaces. This likely is not the best syntax(what does the signature of this Eat(T) method look like, Eat(IConvertible & ICloneable food)?). Likely, a better solution would be to an auto-generated generic type upon the implementing class so that the class definition would be something like:

class HungryWolf:
    ICanEat<ICloneable>, 
    ICanEat<IConvertible>, 
    ICanEat<TGenerated_ICloneable_IConvertible>
        where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable {
    // implementation
}

在IL将不得不改变,才能够让接口实现类型成为像泛型类的 callvirt 指令构造:

.class auto ansi nested private beforefieldinit HungryWolf 
    extends 
        [mscorlib]System.Object
    implements 
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>,
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>,
        class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>

在CLR将不得不通过构建一个接口实现 HungryWolf 与<$ C $处理 callvirt 的说明C>字符串作为泛型类型参数 TGenerated_ICloneable_IConvertible ,并检查是否匹配比其他的接口实现更好的。

The CLR would then have to process callvirt instructions by constructing an interface implementation for HungryWolf with string as the generic type parameter for TGenerated_ICloneable_IConvertible, and checking to see if it matches better than the other interface implementations.

有关协方差,这一切会更简单,因为需要额外的接口来实现也不会是一般类型参数与约束的而只是比其他两种类型之间最派生基类型,这是在编译时已知。

For covariance, all of this would be simpler, since the extra interfaces required to be implemented wouldn't have to be generic type parameters with constraints but simply the most derivative base type between the two other types, which is known at compile time.

如果同一接口被实现的两倍以上,则需要实现额外接口的数量呈指数增长,而这将是柔韧性和实现多个逆变的类型安全性(或协变)的上一个单一的成本类。

If the same interface is implemented more than twice, then the number of extra interfaces required to be implemented grows exponentially, but this would be the cost of the flexibility and type-safety of implementing multiple contravariant(or covariant) on a single class.

我怀疑这将使它成为了框架,但它是我的preferred的解决方案,尤其是新的语言的复杂性将永远自足到希望做的是目前危险的类。

I doubt this will make it into the framework, but it would be my preferred solution, especially since the new language complexity would always be self-contained to the class which wishes to do what is currently dangerous.

编辑:
感谢 Jeppe 提醒我,协方差不超过逆变简单,由于事实上,常见的接口,还必须考虑到。在字符串的情况下的char [] ,集最大的共同点是{对象 ICloneable 的IEnumerable&LT;焦炭&GT; }(的IEnumerable 覆盖的由的IEnumerable&LT;焦炭&GT; )。

edit:
Thanks Jeppe for reminding me that covariance is no simpler than contravariance, due to the fact that common interfaces must also be taken into account. In the case of string and char[], the set of greatest commonalities would be {object, ICloneable, IEnumerable<char>} (IEnumerable is covered by IEnumerable<char>).

然而,这将要求对接口一般类型参数约束的新的语法,以指示该一般类型参数只需要

However, this would require a new syntax for the interface generic type parameter constraint, to indicate that the generic type parameter only needs to

  • 从指定的类或类实现指定接口的至少一个继承
  • 在指定的接口实现了至少一个

可能是这样的:

interface ICanReturn<out T> where T: class {
}

class ReturnStringsOrCharArrays: 
    ICanReturn<string>, 
    ICanReturn<char[]>, 
    ICanReturn<TGenerated_String_ArrayOfChar>
        where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> {
}

泛型类型参数 TGenerated_String_ArrayOfChar 在这种情况下(其中一个或多个接口是常见的)总是被视为对象,即使公共基类已经从对象;因为普通型可以实现一个共同的接口,而不从公共基类继承

The generic type parameter TGenerated_String_ArrayOfChar in this case(where one or more interfaces are common) always have to be treated as object, even though the common base class has already derived from object; because the common type may implement a common interface without inheriting from the common base class.

这篇关于没有警告或错误(或运行时故障)时,逆变导致歧义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆