当反转导致歧义时,不会有警告或错误(或运行时失败)

首先,记住一个.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"); 

编译这个时, 不会收到编译器错误或警告。 运行时,看起来像所调用的方法取决于我的HungryWolf class声明中的接口列表的顺序。 (尝试用逗号( , )分隔的列表交换两个接口。)

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

我可能不是第一个拿出这样的代码。 我使用了接口的逆变 ,但是你可以用接口的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!"); } } 

testing它:

 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声明中的接口列表顺序。 但是我认为更糟糕的原因是这次有人可能会认为重载parsing总是会selectSystemException因为它比Exception更具体。


赏金之前的状态:两个用户三个答案。

奖金的最后一天的状态:仍然没有收到新的答案。 如果没有答案,我将不得不把赏金奖给本·摩达。

我相信编译器会在VB.NET中做出更好的警告,但我仍然认为这样做还不够。 不幸的是,“正确的事情”可能要么不允许有潜在有用的东西(实现与两个协变或相反的genericstypes参数相同的接口),或者引入一些新的语言。

就目前而言,除了HungryWolf类以外,编译器现在可以分配一个错误。 这是一个class级声称要知道如何做一些可能会模棱两可的事情。 这是说明

我知道如何吃ICloneable ,或者以某种方式来实现或inheritanceICloneable

而且,我也知道如何以一种特定的方式吃一个IConvertible ,或者任何实现或inheritance它的东西。

然而, 它从来没有说明它应该做什么,如果它从盘子上收到ICloneableIConvertible 。 如果给了一个HungryWolf的实例,这不会给编译器带来任何的HungryWolf ,因为它可以肯定地说: “嘿,我不知道该怎么做! 。 但是,如果给出ICanEat<string>实例,它会给编译器带来ICanEat<string>编译器不知道variables中对象的实际types是什么,只是它确实实现了ICanEat<string>

不幸的是,当一个HungryWolf被存储在这个variables中时,它不明确地实现了两次完全相同的接口。 所以当然,我们不能抛出一个错误试图调用ICanEat<string>.Eat(string) ,因为该方法存在,并且可以放在ICanEat<string>variables中的许多其他对象完全有效( batwad已经提到过在他的答案之一)。

而且,虽然编译器可能会抱怨将一个HungryWolf对象赋值给一个ICanEat<string>variables是不明确的,但它不能阻止它发生在两个步骤中。 一个HungryWolf可以被分配给一个ICanEat<IConvertible>variables,这个variables可以传递给其他方法,最终被分配到一个ICanEat<string>variables中。 这两个都是完全合法的任务 ,编译器不可能抱怨任何一个。

因此,当ICanEat的genericstypes参数是逆变时, 选项一是禁止HungryWolf类实现ICanEat<IConvertible>ICanEat<ICloneable> ,因为这两个接口可以统一。 但是,这样做会使代码无法替代解决方法。

选项二 ,不幸的是, 需要改变编译器 ,IL和CLR。 它允许HungryWolf类实现两个接口,但是它也需要实现接口ICanEat<IConvertible & ICloneable> ,其中genericstypes参数实现了两个接口。 这可能不是最好的语法(这个Eat(T)方法的签名是什么样的, Eat(IConvertible & ICloneable food) ?)。 可能更好的解决scheme是在实现类上自动生成genericstypes,以便类定义类似于:

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

IL将不得不改变,以便能够像callvirt指令的通用类一样构造接口实现types:

 .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的接口实现来处理callvirt指令, string作为HungryWolf的genericstypes参数,并检查它是否匹配比其他接口实现更好。

对于协方差来说,所有这些都会更简单,因为需要实现的额外接口不一定是具有约束的genericstypes参数, 而只是在编译时已知的另外两种types之间的最衍生基types

如果相同的接口被实现了两次以上,那么需要实现的额外接口的数量呈指数增长,但这将是在单个类上实现多个逆变(或协变)的灵活性和types安全性的代价。

我怀疑这会把它变成框架,但这将是我最喜欢的解决scheme,尤其是因为新语言的复杂性总是会自成体系地让那些希望做到目前危险的课程。


编辑:
谢谢Jeppe提醒我说协变并不比反变更简单 ,因为通用接口也必须考虑在内。 在stringchar[]的情况下,最大的公共集合是{ objectICloneableIEnumerable<char> }( IEnumerableIEnumerable<char> 覆盖 )。

但是,这将需要接口genericstypes参数约束的新语法来指示genericstypes参数只需要

  • 从指定的类或实现至less一个指定接口的类inheritance
  • 实现至less一个指定的接口

可能是这样的:

 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> { } 

在这种情况下(其中一个或多个接口是公共的)genericstypes参数TGenerated_String_ArrayOfChar总是必须被视为object ,即使公共基类已经从object派生出来; 因为通用types可以实现通用接口,而不需要从公共基类inheritance

在这种情况下不能生成编译器错误,因为代码是正确的,并且它应该在所有types不能同时从内部typesinheritance的情况下正常运行。 如果您同时从一个类和一个接口inheritance,问题是一样的。 (即在你的代码中使用基础object类)。

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

由于“接口ICustom(Of In T)”中的“In”和“Out”参数,接口“ICustom(Of Foo)”与另一个实现的接口“ICustom(Of Boo)

我同意C#编译器也应该抛出类似的警告。 检查这个堆栈溢出问题 。 Lippert先生证实,运行时会select一个,应该避免这种编程。

这是我刚刚提出的缺乏警告或错误的理由,我甚至没有喝酒!

进一步抽象你的代码,意图是什么?

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

你在喂东西。 野兽究竟如何把它吃到野兽身上呢? 如果它被给了汤,它可能决定使用一把匙子。 它可能会用刀叉吃羊。 它可能会撕裂它的牙齿撕碎。 但重要的是,作为这个阶级的调用者,我们不在乎它是如何吃的,我们只是想要它被吃掉。

最终由兽决定如何吃东西。 如果野兽决定可以吃一个ICloneable和一个IConvertible话,那么野兽就会知道如何处理这两种情况。 为什么要掌握这些动物如何吃饭呢?

如果确实会导致编译时错误,那么实现一个新的界面是一个突破性的改变。 例如,如果您将HungryWolf作为ICanEat<ICloneable>出货,然后在几个月后添加ICanEat<IConvertible>那么您将使用实现了这两个接口的types来破坏它的每个地方。

而且这两种方式。 如果通用的论证只能实现ICloneable ,那么对于狼来说就会非常愉快。 testing通过,发货,非常感谢沃尔夫先生。 明年你IConvertible你的types添加IConvertible支持,并且BANG! 没有那么有用的一个错误,真的。

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

忍受着我。

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

没有错误,因为ICanEat<T>只包含一个名为Eat方法。 但是请说出来,你得到一个错误:

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

HungryWolf是暧昧的,而不是ICanEat<T> 。 在期望一个编译器错误时,你要求编译器在调用Eat的地方查看variables的值,如果模糊不清,那么它就不一定是可以推断的东西。 考虑一个只实现ICanEat<string>类。

 ICanEat<string> beast; if (somethingUnpredictable) beast = new HungryWolf(); else beast = new UnambiguousCow(); beast.Eat("dinner"); 

在哪里以及如何提出编译器错误?