-->

没有警告或错误(或运行时故障)时,逆变导致歧义(No warning or error (or ru

2019-08-03 05:30发布

首先,记住一个.NET StringIConvertibleICloneable

现在,考虑下面很简单的代码:

//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!");
  }
}

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

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

当一个编译此,一个得不到的编译器错误或警告。 当运行它,它看起来像调用该方法依赖于我的接口列表的顺序class声明HungryWolf 。 (尝试更换的逗号两个接口( , )分隔的列表。)

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

我可能不是第一个拿出这样的代码。 我使用的接口的逆变 ,但你可以与接口的covarainace一个完全类似的例子。 而事实上利珀特先生就是这样做的很久以前。 在他的博客的评论,几乎所有人都认为它应该是一个错误。 然而,他们让这个默默。 为什么?

---

扩展问题:

上面我们利用一个StringIconvertible (接口)和ICloneable (接口)。 无论这两个接口从其他派生的。

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

请记住, StackOverflowException既是SystemException (直接基类)和Exception (基类基类的)。 然后(如果ICanEat<>是像以前一样):

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!");
  }
}

与测试:

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
}

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


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

在赏金的最后一天状态:仍然没有收到新的答案。 如果没有答案露面,我得奖励赏金穆斯林奔Dhaou。

Answer 1:

我相信编译器在VB.NET与警告更好的事情,但我仍然不认为这会远远不够。 不幸的是,“正确的事”可能需要或者禁止的东西,是潜在有用的(有两个协变或逆变泛型类型参数实现相同的接口),或引入新的东西的语言。

既然这样,也没有地方编译器可以分配,现在除了一个错误HungryWolf类。 这是在哪个阶级声称知道如何做一些事情,可能是模糊的点。 它说明

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

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

但是,从来没有规定如果收到其板的东西, 既是 它应该做的事情 ICloneableIConvertible 。 这不会导致编译任何悲伤,如果它被赋予的一个实例HungryWolf ,因为它可以肯定地说:“嘿,我不知道在这里做什么!” 。 但是,当它被赋予它会给编译器悲痛ICanEat<string>实例。 编译器有不知道该对象的变量的实际类型,只知道它肯定不会实施ICanEat<string>

不幸的是,当HungryWolf存储在该变量,它含糊实现了完全相同的接口的两倍。 那么当然,我们不能把试图调用错误ICanEat<string>.Eat(string) ,因为该方法存在,将是非常有效的,其可以被放置在许多其他物体ICanEat<string>变量( batwad已经提到了这一点在他的答案之一)。

此外,尽管编译器可以抱怨一个的分配HungryWolf对象到ICanEat<string>变量是模糊的,它不能阻止它在两个步骤中发生。 甲HungryWolf可以分配给一个ICanEat<IConvertible>变量,它可以被周围传递到其他的方法,并最终分配到ICanEat<string>变量。 这两个是完全合法的分配 ,这将是不可能的编译器抱怨任何一个。

因此, 选择一个是不允许的HungryWolf从实现类既ICanEat<IConvertible>ICanEat<ICloneable>ICanEat的一般类型参数是逆变的,因为这两个接口可以统一。 然而,这消除了代码的东西 ,没有其他解决办法有用的能力

方案二 ,不幸的是,就需要改变编译器 ,无论是IL和CLR。 这将允许HungryWolf类来实现这两个接口,但它也需要接口的实现ICanEat<IConvertible & ICloneable>接口,其中泛型类型参数实现了这两个接口。 这可能不是最好的语法(请问这个签名Eat(T)方法的样子, Eat(IConvertible & ICloneable food) ?)。 可能的是,一个更好的解决办法是自动生成的泛型类型在实现类,使类的定义是这样的:

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将不得不处理callvirt通过构建一个接口实现的说明HungryWolfstring作为泛型类型参数TGenerated_ICloneable_IConvertible ,并检查看它是否符合比其他接口实现更好的。

为协方差,所有的这将是更简单的,因为需要实现额外的接口就不必是通用类型参数与约束,但是简单地在两个其他类型之间的最衍生物基类型 ,它是在编译时已知的。

如果相同的接口被实现两次以上,则需要实现额外的接口数量呈指数级增长,但这将是灵活性和实施一个类中的多个逆变(或协)的类型安全的成本。

我怀疑这将使它成为了框架,但它会是我的首选解决方案以来,特别是新的语言的复杂性将永远是自包含的,其希望做的是目前危险的类。


编辑:
感谢叶普提醒我, 协方差为不超过逆变简单 ,由于这样的事实,常用接口也必须加以考虑。 在的情况下, stringchar[]该组的最大共同性。将{ objectICloneableIEnumerable<char> }( IEnumerable覆盖 IEnumerable<char> )。

然而,这需要对接口泛型类型参数约束一种新的语法,以表明泛型类型参数只需要

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

也许是这样的:

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在这种情况下(其中一个或多个接口是常见的)总是都被当作object ,即使共同的基类已经衍生自object ; 因为共同的类型可以实现一个共同的接口而不从公共基类继承



Answer 2:

不可能在这种情况下,可以生成编译器错误,因为代码是正确的,它应该不会来自内部的类型,同时继承了所有类型的运行正常。 问题是相同的,如果你从一个类,并在同一时间的接口继承。 (即使用基础object在代码中的类)。

出于某种原因,VB.Net编译器会扔在这种情况下,类似的警告

接口“iCustom而(共美孚)”不明确与另一实现的接口“iCustom而(共BOO)”由于“在”和“停止”在参数“界面iCustom而(在T)”

我认为,C#编译器应该抛出了类似的警告,以及。 检查这个堆栈溢出问题 。 利珀特先生证实,运行时将选择一个,应该避免这种编程。



Answer 3:

下面是在正当理由没有警告或错误的,我只是想出了一个尝试,我还没有在喝!

摘要你的代码进一步,是什么用意?

ICanEat<string> beast = SomeFactory.CreateRavenousBeast();
beast.Eat("sheep");

你喂养的东西。 如何兽居然吃起来兽。 如果它被赋予汤,它可能会决定使用勺子。 它可能会使用刀叉吃的羊。 这可能撕了用牙齿撕成碎片。 但问题是这节课,我们不关心它是如何吃的主叫方,我们只是希望它喂食。

最终,它是由兽来决定如何吃什么它被赋予。 如果兽决定是可以吃的ICloneableIConvertible然后它是由兽弄清楚如何处理这两种情况。 猎场看守人为什么要关心动物如何吃餐点?

如果它确实会导致你做出实施一个新的界面是一个重大更改编译时错误。 例如,如果你出货HungryWolf作为ICanEat<ICloneable>然后几个月后添加ICanEat<IConvertible>你打破的每一个地方它与实现两种接口类型使用的地方。

它的两面性。 如果泛型参数只实现ICloneable它将与狼合作相当愉快。 测试通过,船是,非常感谢你沃尔夫先生。 明年你添加IConvertible支持你的类型和BANG! 这并不是说有用的错误,真的。



Answer 4:

另一种解释:没有歧义,因此没有编译器警告。

多多包涵。

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

没有错误,因为ICanEat<T>仅包含一个称为方法Eat与那些参数。 而是说,这和得到一个错误:

HungryWolf wolf = new HungryWolf();
wolf.Eat("sheep");

HungryWolf是模糊的,不ICanEat<T> 在期待一个编译器错误你问的编译器来看待处该点的变量值Eat被称为而努力,如果它是模糊的,它未必是可以它可以推断出。 考虑一类UnambiguousCow其中只实现ICanEat<string>

ICanEat<string> beast;

if (somethingUnpredictable)
    beast = new HungryWolf();
else
    beast = new UnambiguousCow();

beast.Eat("dinner");

在哪里,你怎么会想到要提出的一个编译器错误?



文章来源: No warning or error (or runtime failure) when contravariance leads to ambiguity