c# - 当两个方法在替换类型参数后具有相同的签名时,错误的重载将被覆盖

标签 c# generics compiler-construction overriding overload-resolution

我们相信这个例子展示了 C# 编译器中的一个错误(如果我们错了,请取笑我)。这个错误可能是众所周知的:毕竟,我们的例子是对 in this blog post 所描述内容的简单修改。 .

using System;

namespace GenericConflict
{
  class Base<T, S>
  {
    public virtual int Foo(T t)
    { return 1; }
    public virtual int Foo(S s)
    { return 2; }

    public int CallFooOfT(T t)
    { return Foo(t); }
    public int CallFooOfS(S s)
    { return Foo(s); }
  }

  class Intermediate<T, S> : Base<T, S>
  {
    public override int Foo(T t)
    { return 11; }
  }

  class Conflict : Intermediate<string, string>
  {
    public override int Foo(string t)
    { return 101;  }
  }


  static class Program
  {
    static void Main()
    {
      var conflict = new Conflict();
      Console.WriteLine(conflict.CallFooOfT("Hello mum"));
      Console.WriteLine(conflict.CallFooOfS("Hello mum"));
    }
  }
}
这个想法只是创建一个类 Base<T, S>使用两个虚方法,它们的签名将在“邪恶”选择 T 后变得相同和 S .类(class)Conflict仅重载其中一个虚方法,并且由于 Intermediate<,> 的存在,应该明确定义哪一个!
但是当程序运行时,输出似乎显示错误的重载被覆盖了。
当我们阅读 Sam Ng 的 follow-up post我们得到的表达是那个错误没有被修复,因为他们认为总是会抛出类型加载异常。但在我们的示例中,代码编译并运行时没有错误(只是意外的输出)。

2020 年新增:这在更高版本的 C# 编译器(Roslyn?)中得到纠正。当我问这个问题时,输出是:
11
101
截至 2020 年,tio.runthis output :
101
2

最佳答案

We believe this example exhibits a bug in the C# compiler.



当出现编译器错误时,让我们做我们应该做的事情:仔细对比预期和观察到的行为。

观察到的行为是程序分别产生 11 和 101 作为第一个和第二个输出。

什么是预期的行为?有两个“虚拟插槽”。第一个输出应该是调用Foo(T)中的方法的结果插槽。第二个输出应该是调用Foo(S)中的方法的结果插槽。

这些插槽中有什么?

Base<T,S> 的实例中return 1方法在 Foo(T)插槽和 return 2方法在 Foo(S)插槽。

Intermediate<T,S> 的实例中return 11方法在 Foo(T)插槽和 return 2方法在 Foo(S)插槽。

希望到目前为止你同意我的看法。

Conflict 的实例中,有四种可能:
  • 可能性一:return 11方法在 Foo(T)插槽和 return 101方法在 Foo(S)插槽。
  • 可能性二:return 101方法在 Foo(T)插槽和 return 2方法在 Foo(S)插槽。
  • 可能性三:return 101方法进入两个插槽。
  • 可能性四:编译器检测到程序有歧义,报错。

  • 根据规范的第 10.6.4 节,您预计这里会发生两件事之一。要么:
  • 编译器会判断Conflict中的方法覆盖 Intermediate<string, string> 中的方法,因为首先找到中间类中的方法。在这种情况下,可能性二是正确的行为。或:
  • 编译器会判断Conflict中的方法关于它覆盖哪个原始声明是不明确的,因此可能性四是正确的。

  • 在这两种情况下,可能性都不正确。

    我承认并不是 100% 清楚,这两者中哪一个是正确的。我个人的感觉是,更明智的行为是将覆盖方法视为中间类的私有(private)实现细节;我心中的相关问题不是中间类是否覆盖了基类方法,而是它是否声明了一个具有匹配签名的方法。在这种情况下,正确的行为是选择可能性四。

    编译器实际所做的是您所期望的:它选择了可能性二。因为中间类有一个匹配的成员,我们选择它作为“要覆盖的东西”,不管该方法没有在中间类中声明。编译器确定 Intermediate<string, string>.Foo是被 Conflict.Foo 覆盖的方法,并相应地发出代码。它不会产生错误,因为它判断程序没有错误。

    因此,如果编译器正确地分析了代码,选择了可能性二,并且没有产生错误,那么为什么在运行时编译器似乎选择了可能性一,而不是可能性二?

    因为制作一个在泛型构造下导致两种方法统一的程序是运行时的实现定义行为 .在这种情况下,运行时可以选择做任何事情!它可以选择给出类型加载错误。它可能会产生可验证性错误。它可以选择允许该程序,但根据自己选择的某些标准填充插槽。事实上,后者就是它的作用。运行时会查看 C# 编译器发出的程序,并自行决定一种可能性是分析该程序的正确方法。

    所以,现在我们有一个相当哲学的问题,这是否是一个编译器错误;编译器遵循规范的合理解释,但我们仍然没有得到我们期望的行为。从这个意义上说,它在很大程度上是一个编译器错误。 编译器的工作是将用 C# 编写的程序翻译成用 IL 编写的完全等效的程序 .编译器没有这样做;它将用 C# 编写的程序翻译成用 IL 编写的程序,该程序具有实现定义的行为,而不是 C# 语言规范指定的行为。

    正如 Sam 在他的博客文章中清楚地描述的那样,我们很清楚 C# 语言赋予哪些类型的拓扑具有特定含义与 CLR 赋予哪些拓扑具有特定含义之间的这种不匹配。 C# 语言很清楚,可能性 2 可以说是正确的,但我们无法发出任何代码让 CLR 这样做,因为 CLR 从根本上具有实现定义的行为,只要两个方法统一以具有相同的签名。因此我们的选择是:
  • 什么都不做。允许这些疯狂的、不切实际的程序继续具有与 C# 规范不完全匹配的行为。
  • 使用启发式方法。正如 Sam 所指出的,我们可以更聪明地使用元数据机制来告诉 CLR 哪些方法覆盖了哪些其他方法。但是... 这些机制使用方法签名来消除歧义情况 现在我们又回到了以前的同一条船上;我们现在正在使用一种具有实现定义行为的机制来消除具有实现定义行为的程序的歧义!这是一个非首发。
  • 每当编译器可能发出其行为由运行时实现定义的程序时,都会导致编译器产生警告或错误。
  • 修复 CLR,以便导致方法在签名中统一的类型拓扑的行为是明确定义的,并且与 C# 语言的行为相匹配。

  • 最后一种选择非常昂贵。支付这笔费用给我们带来了微乎其微的用户利益,并直接从解决用户编写合理程序所面临的现实问题中抽走了预算。无论如何,这样做的决定完全不在我的掌控之中。

    因此,我们 C# 编译器团队选择采用第一种和第三种策略的组合;有时我们会针对这种情况产生警告或错误,有时我们什么都不做,让程序在运行时做一些奇怪的事情。

    由于在实践中这类程序很少出现在现实的业务线编程场景中,因此我对这些极端情况并没有感到很糟糕。如果它们便宜且易于修复,那么我们会修复它们,但它们既不便宜也不易于修复。

    如果您对这个主题感兴趣,请参阅我的文章,该文章介绍了另一种方法,在该方法中,使两种方法统一会导致警告和实现定义的行为:

    http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx

    http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx

    关于c# - 当两个方法在替换类型参数后具有相同的签名时,错误的重载将被覆盖,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10177571/

    相关文章:

    java - 如何在 Java 中创建通用列表?

    java - 打印java通用二叉搜索树中的节点元素

    c++ - 是Visual C++ intellisense "smarter"比编译器

    c# - 线程信号基础

    c# - 如何在 ascx 页面中声明一个隐藏字段,以便它从 aspx 中的隐藏字段中获取值?

    c# - 在命令行针对 .vdproj 运行 devenv 不会产生 MSI

    java - 如何重载泛型构造函数

    c# - 如何在微软新的Visual Studio Code中编译c#?

    java - 如何从我在运行时使用 ASM 动态创建的 Java 类中获取和使用类类型?

    cocoa - 将源文件添加到不用于编译的 cocoa 项目