【问题标题】:C# Method overload resolution not selecting concrete generic overrideC#方法重载决议没有选择具体的泛型覆盖
【发布时间】:2017-03-29 23:09:26
【问题描述】:

这个完整的 C# 程序说明了这个问题:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

我遇到了 StackOverlowException,我可以追溯到这个调用模式,我试图将调用转发到更具体的重载。令我惊讶的是,调用并没有选择更具体的重载,而是回调自身。它显然与通用的基本类型有关,但我不明白为什么它不会选择 Execute(string) 重载。

有人对此有任何见解吗?

上面的代码是为了显示模式而简化的,实际的结构有点复杂,但问题是一样的。

【问题讨论】:

  • 不太确定确定方法分辨率的规范,但暂时的解决方法是((Executor&lt;string&gt;)this).Execute((string)item).. 这非常丑陋。
  • 根据this question 应该选择最具体的重载。
  • @Steve 这与此处介绍的情况不同,因为它不处理覆盖,C# 规范专门以不同方式处理这种特殊情况。
  • @DavidL 似乎如此。我不知道,很有趣。

标签: c# generics overload-resolution


【解决方案1】:

看起来这在 C# 规范 5.0、7.5.3 重载解决方案中有所提及:

重载解析选择要在 C# 中以下不同上下文中调用的函数成员:

  • 调用以调用表达式命名的方法 (§7.6.5.1)。
  • 调用以对象创建表达式命名的实例构造函数(第 7.6.10.1 节)。
  • 通过元素访问调用索引器访问器(第 7.6.6 节)。
  • 调用表达式中引用的预定义或用户定义的运算符(第 7.3.3 节和第 7.3.4 节)。

这些上下文中的每一个都定义了一组候选函数成员 以及以自己独特的方式列出的参数,如 上面列出的部分中的详细信息。例如,集合 方法调用的候选不包括标记的方法 覆盖(第 7.4 节),并且基类中的方法不是候选者(如果有的话) 派生类中的方法适用(第 7.6.5.1 节)。

当我们查看 7.4 时:

在类型 T 中使用 K 类型参数对名称 N 的成员查找处理如下:

• 首先,确定一组名为 N 的可访问成员:

  • 如果 T 是类型参数,则集合是集合的并集
    指定为 T 的主要约束或次要约束(第 10.1.5 节)的每个类型中名为 N 的可访问成员,以及对象中名为 N 的可访问成员集。

  • 否则,该集合由 T 中名为 N 的所有可访问(第 3.5 节)成员组成,包括继承成员和对象中名为 N 的可访问成员。如果 T 是构造类型,则成员集 通过替换类型参数获得,如 §10.3.2 中所述。 包含覆盖修饰符的成员被排除在集合之外。

如果您删除 override,编译器会在您投射项目时选择 Execute(string) 重载。

【讨论】:

  • 当然我的后续问题是:为什么?我确信这是有充分理由的,但这对我来说似乎是令人惊讶的行为。
  • @Mark Jon Skeet 的article 描述了原因,“这样做的原因是为了降低脆弱的基类问题的风险,在基类中引入新方法可能会导致问题对于从它派生的类的消费者。”
  • @khargoosh Skeet 的解释处理了一个不同的问题(为什么父类中的方法即使更合适也会被忽略)。脆弱的基类问题显然不适用于这里,因为我们正在处理添加到子类而不是基类的函数。将行为更改为也考虑同一类中的覆盖方法,根本不会改变向基类添加方法时的行为(这是脆弱的基类问题所处理的)。
  • Jon 实际上继续指出后面提到的问题,但没有提及任何原因,并称这种行为“特别令人惊讶”。
  • @Voo:这可能令人惊讶,但同样是设计使然。看我的回答。
【解决方案2】:

正如 Jon Skeet 的 article on overloading 中所提到的,当在一个类中调用一个方法时,该方法还覆盖了基类中的同名方法,编译器将始终采用类内方法而不是覆盖,无论类型的“特异性”,前提是签名是“兼容的”。

Jon 继续指出,这是避免跨继承边界重载的一个很好的论据,因为这正是可能发生的意外行为。

【讨论】:

  • 确实,它与泛型基类没有任何关系。我可以在没有代码方面的情况下进行复制。这让我很惊讶。
  • 正确,通用参数在这种情况下是一个红鲱鱼。
  • @Mark:有种方法来构造重载解决问题,其中泛型或缺乏泛型被用作决胜局,但这不是其中之一。例如,给定class C { void M(int){} void M&lt;T&gt;(T){} } 和对c.M(123) 的调用,非泛型将获胜。还有比这更微妙的决胜局;有关详细信息,请参阅规范。
【解决方案3】:

正如其他答案所指出的,这是设计使然。

让我们考虑一个不太复杂的例子:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

问题是为什么giraffe.Eat(apple) 解析为Giraffe.Eat(Food) 而不是虚拟Animal.Eat(Apple)

这是两个规则的结果:

(1) 在解析重载时,接收者的类型比任何参数的类型更重要。

我希望很清楚为什么必须如此。编写派生类的人比编写基类的人拥有更多的知识,因为编写派生类的人使用的是基类,反之则不然。

Giraffe 的人说“我有办法让Giraffe任何食物”,这需要长颈鹿消化内部的特殊知识。该信息不存在于基类实现中,它只知道如何吃苹果。

因此,重载决策应始终优先选择派生类的适用方法,而不是选择基类的方法,无论参数类型转换是否更好。

(2) 选择覆盖或不覆盖虚拟方法不是类的公共表面区域的一部分。这是一个私有的实现细节。因此,在执行可能会根据方法是否被覆盖而改变的重载决议时,不必做出任何决定。

重载解析绝对不能说“我要选择虚拟Animal.Eat(Apple)因为它被覆盖了”。

现在,您可能会说“好吧,假设我在打电话时 Giraffe 里面。”代码inside Giraffe 拥有私有实现细节的所有知识,对吧?所以当遇到giraffe.Eat(apple) 时,它可以决定调用虚拟Animal.Eat(Apple) 而不是Giraffe.Eat(Food),对吧?因为它知道有一个实现可以理解吃苹果的长颈鹿的需求。

这是一种比疾病更糟糕的治疗方法。现在我们遇到了相同代码具有不同行为的情况,这取决于它在哪里运行!你可以想象在类外调用giraffe.Eat(apple),重构它使其在类内,然后突然可观察到的行为发生变化!

或者,你可能会说,嘿,我意识到我的 Giraffe 逻辑实际上足够通用,可以转移到基类,但不能转移到 Animal,所以我将重构我的 Giraffe 代码:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

现在所有对giraffe.Eat(apple) inside Giraffe 的调用在重构后突然不同 重载解析行为?那将是非常出乎意料的!

C# 是一门成功的语言;我们非常希望确保简单的重构(例如更改层次结构中方法被覆盖的位置)不会导致行为发生细微的变化。

总结:

  • 重载解析优先于接收器而不是其他参数,因为调用了解接收器内部的专用代码比调用不了解接收器的更通用代码更好。
  • 在重载解析期间不考虑是否以及在何处重写方法;出于重载决议的目的,所有方法都被视为从未被覆盖。这是一个实现细节,而不是类型表面的一部分。
  • 重载解析问题已解决——当然是模可访问性! -- 无论代码中哪里出现问题,都一样。我们没有一种算法来解决接收者属于包含代码的类型,而另一种算法用于解决调用属于不同的类。

有关相关问题的其他想法可以在这里找到:https://ericlippert.com/2013/12/23/closer-is-better/ 和这里https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多