43

首先,请记住 .NETString既是.NETIConvertible又是ICloneable.

现在,考虑以下非常简单的代码:

//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。(尝试交换逗号 ( ,) 分隔列表中的两个接口。)

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

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

---

扩展问题:

上面我们利用aString既是Iconvertible(接口)又是ICloneable(接口)。这两个接口都不是从另一个派生的。

现在这是一个基类的例子,从某种意义上说,它有点糟糕。

请记住,aStackOverflowException既是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.


赏金开启前的状态:两位用户的三个回答。

赏金最后一天的状态:仍然没有收到新的答案。如果没有答案出现,我将不得不将赏金奖励给穆斯林 Ben Dhaou。

4

4 回答 4

9

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

就目前而言,除了类之外,编译器现在没有其他地方可以分配错误HungryWolf。这就是一个班级声称知道如何做一些可能模棱两可的事情的时候。它在说明

我知道如何以ICloneable某种方式吃一个,或任何实现或继承它的东西。

而且,我也知道如何以IConvertible某种方式吃一个,或任何实现或继承它的东西。

但是,它从不说明如果它在它的盘子上收到既是 an又是 an 的东西应该做什么。如果给定一个 的实例,这不会给编译器带来任何痛苦,因为它可以肯定地说“嘿,我不知道在这里做什么!” . 但是当它被赋予实例时,它会给编译器带来痛苦。编译器不知道变量中对象的实际类型是什么,只知道它确实实现了.ICloneableIConvertibleHungryWolfICanEat<string>ICanEat<string>

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

此外,尽管编译器可能会抱怨将HungryWolf对象分配给ICanEat<string>变量是不明确的,但它不能阻止它分两步发生。AHungryWolf可以分配给一个ICanEat<IConvertible>变量,该变量可以传递给其他方法并最终分配给一个ICanEat<string>变量。这两个都是完全合法的赋值,编译器不可能抱怨其中任何一个。

因此,选项一是禁止HungryWolf类同时实现ICanEat<IConvertible>ICanEat<ICloneable>whenICanEat的泛型类型参数是逆变的,因为这两个接口可以统一。但是,这消除了编写有用代码的能力,而没有其他解决方法。

不幸的是,选项二需要更改编译器,包括 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 必须通过为withcallvirt构造一个接口实现作为 的泛型类型参数来处理指令,并检查它是否比其他接口实现匹配得更好。HungryWolfstringTGenerated_ICloneable_IConvertible

对于协方差,所有这些都会更简单,因为需要实现的额外接口不必是具有约束的泛型类型参数,而只是其他两种类型之间最衍生的基本类型,这在编译时是已知的。

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

我怀疑这是否会进入框架,但这将是我首选的解决方案,特别是因为新的语言复杂性对于希望执行当前危险操作的类而言始终是自包含的。


编辑:
感谢Jeppe提醒我covariance 并不比 travariance 简单,因为还必须考虑公共接口。在 和 的情况下stringchar[]最大共性的集合是 { object, ICloneable, IEnumerable<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; 因为公共类型可以实现一个公共接口而不继承公共基类

于 2013-01-06T17:08:10.583 回答
6

在这种情况下无法生成编译器错误,因为代码是正确的,并且它应该可以在所有不同时从两个内部类型继承的类型上正常运行。如果同时从类和接口继承,问题是一样的。(即object在您的代码中使用基类)。

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

由于 'Interface ICustom(Of In T)' 中的 'In' 和 'Out' 参数,接口 'ICustom(Of Foo)' 与另一个实现的接口 'ICustom(Of Boo)' 不明确

我同意 C# 编译器也应该发出类似的警告。检查这个堆栈溢出问题。Lippert 先生确认运行时会选择一个,并且应该避免这种编程。

于 2012-11-27T15:33:47.740 回答
4

这是我刚刚提出的缺乏警告或错误的一种尝试,我什至没有喝酒!

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

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

你在喂东西。野兽实际上是如何把它吃掉的。如果给它汤,它可能会决定用勺子。它可能会用刀叉来吃羊。它可能会用牙齿把它撕成碎片。但关键是作为这个类的调用者,我们不在乎它如何吃,我们只希望它被喂食。

最终由野兽来决定如何吃它所给予的东西。如果野兽决定它可以吃 anICloneable和 anIConvertible然后由野兽来决定如何处理这两种情况。为什么猎场看守人要关心动物如何进食?

如果它确实导致了编译时错误,那么您将实现一个新接口作为一项重大更改。例如,如果您发布HungryWolfICanEat<ICloneable>,然后几个月后添加ICanEat<IConvertible>,您会破坏使用实现这两个接口的类型的每个地方。

它是双向的。如果泛型参数只实现ICloneable它会很高兴与狼一起工作。测试通过,发货,非常感谢 Wolf 先生。明年您将IConvertible支持您的类型和 BANG!一个错误并没有多大帮助,真的。

于 2012-11-27T16:21:05.530 回答
2

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

忍受我。

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

您希望在哪里以及如何引发编译器错误?

于 2012-11-28T11:29:05.830 回答