为什么C#编译器在静态方法调用实例方法的地方不会出错?


110

以下代码具有一个静态方法Foo(),它调用实例方法Bar()

public sealed class Example
{
    int count;

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

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

它编译时没有错误*,但在运行时生成运行时绑定程序异常。如预期的那样,将动态参数删除到这些方法会导致编译器错误。

那么,为什么要使用动态参数才能编译代码?ReSharper也不将其显示为错误。

编辑1: *在Visual Studio 2008中

编辑2:已添加,sealed因为子类可能包含静态Bar(...)方法。当无法在运行时调用实例方法以外的其他任何方法时,即使是密封版本也可以编译。


8
为非常好的问题+1
cuongle 2012年

40
这是一个Eric-Lippert问题。
Olivier Jacot-Descombes

3
我敢肯定乔恩斯基特会知道如何处理这个藏汉寿做;)@ OlivierJacot-Descombes

2
@Olivier,乔恩·斯凯特(Jon Skeet)可能希望对代码进行编译,因此编译器允许:
Mike Scott

5
这是为什么dynamic除非真正需要否则不应该使用的另一个示例。
Servy 2012年

Answers:


71

更新:以下答案是在C#7.3(2018年5月)引入之前于2012年编写的。在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原始问题示例的解释是,为了保持一致,当类型是动态的时,我们还首先找到了最佳的重载(仅检查参数编号和参数类型等,而不是静态与非)。 -static),然后再检查是否为静态。但这意味着静态检查必须等到运行时才能执行。因此观察到的行为。

后期补充:Eric Lippert此博客中可以推断出他们为什么选择做这种滑稽的事情的背景。


原始问题没有重载。显示静态过载的答案并不重要。回答“好吧,如果您写了这个……”是无效的,因为我没有写过:-)
Mike Scott

5
@MikeScott我只是想说服您,C#中的重载分辨率始终是这样的:(1)无论静态/非静态,都找到最佳匹配。(2)现在我们知道要使用什么重载,然后检查静态。因此,当dynamic以这种语言进行介绍时,我认为C#的设计师说:“当它是一种dynamic表达式时,我们不会考虑(2)编译时。” 因此,我的目的是想出为什么他们选择直到运行时才检查静态实例还是静态实例的原因。我会说,此检查发生在绑定时
杰普·斯蒂格·尼尔森

足够公平,但是仍然不能解释为什么在这种情况下编译器无法解决对实例方法的调用。换句话说,编译器执行解析的方式很简单-它无法识别像我的示例这样的简单情况,即不可能解决该调用。具有讽刺意味的是:通过使用带有动态参数的单个Bar()方法,编译器随后将忽略该单个Bar()方法。
迈克·斯科特

45
我编写了C#编译器的这一部分,而Jeppe是正确的。请对此投票。重载解析发生在检查给定方法是静态方法还是实例方法之前,在这种情况下,我们将重载解析推迟到运行时,因此也将静态/实例检查推迟到运行时。此外,编译器会“尽最大努力”静态地找到动态错误,这绝对是不全面的。
克里斯·伯罗斯

30

Foo具有动态的参数“ x”,这意味着Bar(x)是动态表达式。

例如,Example完全有可能具有以下方法:

static Bar(SomeType obj)

在这种情况下,将解决正确的方法,因此语句Bar(x)完全有效。存在实例方法Bar(x)的事实是无关紧要的,甚至没有考虑:根据定义,由于Bar(x)是动态表达式,因此将解析时间推迟到运行时。


14
但是当您取出实例Bar方法时,该方法不再编译。
贾斯汀·哈维

1
@Justin有趣-警告?还是错误?无论哪种方式,它都只能在method-group范围内进行验证,而将完整的重载解析留给运行时。
马克·格雷夫

1
@Marc,因为没有其他Bar()方法,所以您没有回答这个问题。鉴于只有一个Bar()方法没有重载,您能解释一下吗?当无法调用任何其他方法时,为什么要推迟运行时间?还是在那里?注意:我已经编辑了代码以密封该类,该类仍在编译。
迈克·斯科特

1
@mike为何要延迟运行时间:因为这就是动态的含义
Marc Gravell

2
@迈克不可能不是重点; 重要的是是否需要。动态的全部意义在于,它不是编译器的工作。
马克·格雷夫

9

“动态”表达式将在运行时绑定,因此,如果您使用正确的签名或实例方法定义静态方法,编译器将不会对其进行检查。

“正确”的方法将在运行时确定。编译器无法确定运行时是否存在有效的方法。

“ dynamic”关键字是为动态和脚本语言定义的,其中可以随时定义方法,甚至在运行时也可以。疯狂的事情

这里是一个实例,它处理int但没有字符串,因为该方法在实例上。

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.Bar(...)而不是Example.Foo(...)吗?Foo()在您的示例中是否无关紧要?我不太了解您的榜样。为什么添加静态泛型方法会引起问题?您可以编辑答案以包括该方法而不是将其作为选项吗?
Mike Scott

但是我发布的示例只有一个实例方法,没有重载,因此在编译时,您知道没有可能解决的静态方法。只有添加至少一个,情况才会改变并且代码有效。
Mike Scott

但是此示例仍然具有多个Bar()方法。我的示例只有一种方法。因此,不可能调用任何静态Bar()方法。该调用可以在编译时解决。
Mike Scott

@Mike可以是!=是; 如果是动态的,则不需要这样做
Marc Gravell

@MarcGravell,请澄清一下?
Mike Scott
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.