为什么C#编译器不会在静态方法调用实例方法的代码中报错?

110
以下代码有一个静态方法Foo()调用一个实例方法Bar():
public sealed class Example
{
    int count;

    public static void Foo( dynamic x )
    {
        Bar(x);
    }

    void Bar( dynamic x )
    {
        count++;
    }
}

这段代码可以正常编译,但在运行时会生成一个运行时绑定异常。如果将这些方法的动态参数移除,会像预期的那样导致编译器错误。

那么为什么有动态参数会使代码可编译呢?ReSharper也没有显示它为错误。

Edit 1: *在Visual Studio 2008中

Edit 2: 添加了sealed,因为子类可能包含静态的Bar(...)方法。即使是被封闭的版本,在运行时也只会调用实例方法而不会调用其他任何方法,但它仍然可以编译。


8
非常好的问题,点赞! - cuongle
40
这是一个Eric-Lippert式的问题。 - Olivier Jacot-Descombes
3
我很确定Jon Skeet也知道如何处理这个,;) @OlivierJacot-Descombes - Thousand
2
@Olivier,Jon Skeet可能希望代码能够编译,所以编译器允许它 :-)) - Mike Scott
5
这是另一个例子,说明为什么除非确实需要,否则不应使用“dynamic”。 - Servy
显示剩余12条评论
3个回答

71

更新:下面的答案是在2012年编写的,在C# 7.3(2018年5月)引入之前。在C# 7.3中的新功能中,第一项“改进的重载候选”解释了如何改变重载分辨规则,使得非静态重载被提前舍弃了。因此,下面的答案(以及这整个问题)现在基本上只有历史意义!


(在C# 7.3之前:)

由于某些原因,在检查静态与非静态之前,重载分辨总是会找到最佳匹配项,请使用所有静态类型尝试此代码:

class SillyStuff
{
  static void SameName(object o) { }
  void SameName(string s) { }

  public static void Test()
  {
    SameName("Hi mom");
  }
}
这段代码无法编译,因为最佳匹配是接受一个 string 的方法,但这是一个实例方法,所以编译器报错(而不是选择次优匹配)。补充说明一下:我认为原问题中 dynamic 示例的解释是,在类型为动态时为保持一致性,我们也会首先找到最佳匹配(仅检查参数数量和参数类型等信息,不考虑静态与非静态),然后才会检查静态。但这意味着静态检查必须等到运行时才能进行。这就是观察到的行为。追加说明:可以从Eric Lippert的这篇博客文章中推断出他们为什么选择这种有趣的顺序。

原问题中并没有重载。展示静态重载的答案并不相关。回答“如果你写了这个...”是无效的,因为我没有写那个 :-) - Mike Scott
5
@MikeScott,我试图说服你,C#中的重载决议总是按照以下方式进行:(1)查找最佳匹配项,无视静态/非静态。 (2)现在我们知道使用哪种重载,然后检查静态性。因此,当dynamic引入语言时,我认为C#的设计者们说:“当表达式是dynamic类型时,我们不考虑(2)在编译时检查。”因此,我的目的是提出为什么他们选择在运行时而不是编译时检查静态与实例的想法。我会说,这个检查发生在绑定时间 - Jeppe Stig Nielsen
还可以,但这仍然无法解释为什么在这种情况下编译器无法解析对实例方法的调用。换句话说,编译器进行解析的方式是简单化的 - 它不会识别像我的示例那样的简单情况,在这种情况下它无法解析调用。具有动态参数的单个Bar()方法的讽刺是:编译器随后忽略了该单个Bar()方法。 - Mike Scott
45
我编写了C#编译器的这部分代码,Jeppe是正确的。请投票支持此项。过载解析发生在检查给定方法是静态方法还是实例方法之前,在这种情况下,我们将过载解析推迟到运行时,并因此在运行时进行静态/实例检查。此外,编译器会尽力静态查找动态错误,但这绝不是全面的。 - Chris Burrows

30

Foo有一个名为"x"的动态参数,这意味着Bar(x)是一个动态表达式。

Example完全可以拥有如下方法:

static Bar(SomeType obj)

在哪种情况下,正确方法会被解析,所以语句Bar(x)是完全有效的。存在一个实例方法Bar(x)事实上是不相关的,甚至不被考虑:根据定义,由于Bar(x)是动态表达式,我们已经将解析推迟到运行时。


14
但是当你删掉这个实例方法 Bar 后,它就无法编译。 - Justin Harvey
1
@Justin 有趣 - 是一个警告?还是一个错误?无论哪种方式,它可能只验证到方法组,将完整的重载分辨过程留给运行时。 - Marc Gravell
1
@Marc,既然没有另一个Bar()方法,你并没有回答问题。既然只有一个没有重载的Bar()方法,你能解释一下这个吗?在没有其他方法调用的情况下,为什么要推迟到运行时呢?或者还有其他的方法吗?注意:我已经编辑了代码以封闭类,但仍然可以编译。 - Mike Scott
1
@mike 关于为什么要推迟到运行时:因为这就是动态的意思 - Marc Gravell
2
@Mike 不可能并不是关键点; 重要的是它是否必需。动态的整个意义在于那不是编译器的工作。 - Marc Gravell
显示剩余15条评论

9
"dynamic"表达式将在运行时绑定,因此如果您定义了具有正确签名的静态方法或实例方法,则编译器不会检查它。
"正确"方法将在运行时确定。编译器无法知道在运行时是否存在有效方法。
"dynamic"关键字适用于动态和脚本语言,在其中方法可以在任何时候定义,甚至在运行时。 Crazy stuff 这里是一个处理整数但不处理字符串的示例,因为该方法在实例上。
class Program {
    static void Main(string[] args) {
        Example.Foo(1234);
        Example.Foo("1234");
    }
}
public class Example {
    int count;

    public static void Foo(dynamic x) {
        Bar(x);
    }

    public static void Bar(int a) {
        Console.WriteLine(a);
    }

    void Bar(dynamic x) {
        count++;
    }
}

您可以添加一个方法来处理所有“错误”调用,这些调用无法被处理。
public class Example {
    int count;

    public static void Foo(dynamic x) {
        Bar(x);
    }

    public static void Bar<T>(T a) {
        Console.WriteLine("Error handling:" + a);
    }

    public static void Bar(int a) {
        Console.WriteLine(a);
    }

    void Bar(dynamic x) {
        count++;
    }
}

你的例子中,调用代码不应该是Example.Foo(...),而应该是Example.Bar(...),Foo() 在这个例子中不相关。我真的不理解你的例子。为什么添加静态泛型方法会导致问题?你能否编辑你的答案,将该方法包含在内,而不是给它作为一个选项呢? - Mike Scott
但是我发布的示例只有一个实例方法,没有重载,因此在编译时,您知道没有可能解析的静态方法。只有添加至少一个方法才会改变情况,代码才有效。 - Mike Scott
但是这个例子仍然有不止一个Bar()方法。我的例子只有一个方法。因此没有调用任何静态的Bar()方法的可能性。调用可以在编译时解决。 - Mike Scott
@Mike可以不等于is;使用动态语言,这并非必须要这样做。 - Marc Gravell
@MarcGravell,请澄清一下? - Mike Scott
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接