Jep*_*sen 43 .net c# ambiguity contravariance undefined-behavior
首先,请记住.NET String是IConvertible和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!");
}
}
Run Code Online (Sandbox Code Playgroud)
然后尝试以下(在某些方法中):
ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");
Run Code Online (Sandbox Code Playgroud)
当编译它时,没有编译器错误或警告.运行它时,看起来调用的方法取决于我的class声明中的接口列表的顺序HungryWolf.(尝试在逗号(,)分隔列表中交换两个接口.)
问题很简单:这不应该给出编译时警告(或者在运行时抛出)吗?
我可能不是第一个提出像这样的代码的人.我使用了界面的逆变,但你可以用界面的covarainace做一个完全类似的例子.事实上,Lippert先生很久以前就做过这样的事情.在他博客的评论中,几乎每个人都认为这应该是一个错误.然而他们默默地允许这样做.为什么?
---
扩展问题:
上面我们利用一个String既Iconvertible(接口)和ICloneable(接口).这两个接口都不是源于另一个.
现在这里有一个基类的例子,从某种意义上说,它有点糟糕.
请记住,a 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!");
}
}
Run Code Online (Sandbox Code Playgroud)
用以下方法测试:
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
}
Run Code Online (Sandbox Code Playgroud)
仍然没有警告,错误或异常.仍取决于class声明中的接口列表顺序.但我认为情况更糟的原因是,这次有人可能会认为重载决策总是会选择,SystemException因为它更具体而不仅仅是Exception.
赏金开启前的状态:来自两个用户的三个答案.
赏金最后一天的状态:仍未收到新答案.如果没有答案出现,我将不得不将赏金奖给穆斯林本·扎乌.
我相信编译器在VB.NET中做了更好的警告,但我仍然认为这远远不够.不幸的是,"正确的事情"可能要么禁止某些可能有用的东西(用两个协变或逆变泛型类型参数实现相同的接口)或者引入一些新的语言.
就目前而言,除了HungryWolf类之外,编译器现在没有地方可以分配错误.这就是一个阶级声称知道如何做一些可能含糊不清的事情的观点.这是陈述
我知道如何以
ICloneable某种方式吃一种或任何实施或继承的东西.并且,我也知道如何以
IConvertible某种方式吃一种或者任何实现或继承它的东西.
然而,它永远不会说明如果它在它的盘子上收到既是a ICloneable又是a的IConvertible东西应该做什么.如果给出一个实例,这不会导致编译器任何悲伤HungryWolf,因为它可以肯定地说"嘿,我不知道该怎么做!" .但是当给出ICanEat<string>实例时,它会给编译器带来悲痛.编译器不知道变量中对象的实际类型是什么,只是它确实实现了ICanEat<string>.
不幸的是,当a HungryWolf存储在该变量中时,它模糊地实现了两次完全相同的接口.所以当然,我们不能抛出试图调用的错误ICanEat<string>.Eat(string),因为该方法存在并且对于可以放入ICanEat<string>变量的许多其他对象完全有效(batwad已经在他的一个答案中提到了这一点).
此外,尽管编译器可能会抱怨将HungryWolf对象赋值给ICanEat<string>变量是不明确的,但它不能阻止它分两步发生.HungryWolf可以将A 分配给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
}
Run Code Online (Sandbox Code Playgroud)
然后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>
Run Code Online (Sandbox Code Playgroud)
然后,CLR必须callvirt通过构造用作HungryWolfwith string的泛型类型参数的接口实现来处理指令TGenerated_ICloneable_IConvertible,并检查它是否与其他接口实现匹配得更好.
为协方差,所有的这将是更简单的,因为需要实现额外的接口就不必是通用类型参数与约束而是简单地在两个其他类型之间的最衍生物基类型,它是在编译时已知的.
如果相同的接口实现两次以上,则需要实现的额外接口数量呈指数级增长,但这将是在单个类上实现多个逆变(或协变)的灵活性和类型安全性的代价.
我怀疑这会使它进入框架,但它将是我的首选解决方案,特别是因为新的语言复杂性总是对于希望做目前危险的类的自包含.
编辑:
感谢Jeppe提醒我协方差并不比逆变更简单,因为必须考虑通用接口.在的情况下string和char[],设定最大的共同点就是{ 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> {
}
Run Code Online (Sandbox Code Playgroud)
TGenerated_String_ArrayOfChar在这种情况下,通用类型参数(其中一个或多个接口是公共的)总是必须被视为object,即使公共基类已经派生自object; 因为公共类型可以实现公共接口而不从公共基类继承.
在这种情况下无法生成编译器错误,因为代码是正确的,并且它应该可以与所有不同时从两个内部类型继承的类型一起运行.如果您同时从类和接口继承,则问题是相同的.(即object在代码中使用基类).
出于某种原因,VB.Net编译器会在类似的情况下抛出警告
接口'ICustom(Of Foo)'与另一个实现的接口'ICustom(Of Boo)'不明确,因为'接口ICustom(Of In T)中的'In'和'Out'参数
我同意C#编译器也应该发出类似的警告.检查此Stack Overflow问题.Lippert先生确认运行时将选择一个并且应该避免这种编程.
| 归档时间: |
|
| 查看次数: |
917 次 |
| 最近记录: |