gpt4 book ai didi

c# - 当逆变导致歧义时,没有警告或错误(或运行时失败)

转载 作者:IT王子 更新时间:2023-10-29 03:54:56 26 4
gpt4 key购买 nike

首先,请记住一个 .NET String两者都是 IConvertibleICloneable .

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

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

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

我可能不是第一个想出这样的代码的人。我使用了接口(interface)的逆变,但是你可以用接口(interface)的协方差做一个完全类似的例子。事实上 Mr Lippert did just that很久以前。在他博客的评论中,几乎所有人都同意这应该是一个错误。然而,他们默默地允许了这一点。 为什么?

---

扩展问题:

上面我们利用了一个 String两者都是 Iconvertible (接口(interface))和 ICloneable (界面)。这两个接口(interface)都不是从另一个接口(interface)派生的。

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

记住一个 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 中的接口(interface)列表顺序宣言。但我认为更糟的原因是这一次有人可能认为重载决议总是选择 SystemException因为它比 Exception 更具体.

赏金开放前的状态:来自两个用户的三个答案。

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

最佳答案

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

就目前而言,除了 HungryWolf 之外,编译器现在没有任何地方可以分配错误。类(class)。这就是一个类声称知道如何做一些可能不明确的事情的时候。它在说明

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

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



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

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

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

因此, 选项一 是禁止 HungryWolf从实现两个类 ICanEat<IConvertible>ICanEat<ICloneable>ICanEat的泛型类型参数是逆变的,因为这两个接口(interface)可以统一。然而,这个 删除了编写有用内容的能力 没有替代的解决方法。

选项二 不幸的是,会 需要更改编译器 ,IL 和 CLR。它将允许 HungryWolf类来实现两个接口(interface),但它也需要实现接口(interface) ICanEat<IConvertible & ICloneable>接口(interface),其中泛型类型参数实现两个接口(interface)。这可能不是最好的语法(这个 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 的泛型类一样构造接口(interface)实现类型。操作说明:
.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通过为 HungryWolf 构造接口(interface)实现来说明与 string作为 TGenerated_ICloneable_IConvertible 的泛型类型参数,并检查它是否比其他接口(interface)实现更好地匹配。

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

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

我怀疑这是否会使其进入框架,但这将是我的首选解决方案,特别是因为新的语言复杂性总是独立于希望做当前危险的事情的类。

编辑:
感谢 Jeppe提醒我 协方差并不比逆变简单 ,因为还必须考虑通用接口(interface)。在 string的情况下和 char[] ,最大的共同点是 { object , ICloneable , IEnumerable<char> } ( IEnumerableIEnumerable<char> 覆盖)。

但是,这将需要接口(interface)泛型类型参数约束的新语法,以指示泛型类型参数只需要
  • 从指定的类或至少实现指定接口(interface)之一的类继承
  • 至少实现指定接口(interface)之一

  • 可能是这样的:
    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在这种情况下(一个或多个接口(interface)是通用的)总是必须被视为 object ,即使公共(public)基类已经派生自 object ;因为 公共(public)类型可以在不继承公共(public)基类的情况下实现公共(public)接口(interface) .

    关于c# - 当逆变导致歧义时,没有警告或错误(或运行时失败),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13568337/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com