为什么即使只有一个可能的返回类型,方法调用表达式也具有dynamic类型

本文关键字:表达式 调用 方法 类型 dynamic 返回类型 有一个 为什么 | 更新日期: 2023-09-27 18:25:15

受此问题启发。

短版本:如果M只有一个重载,或者M的所有重载都有相同的返回类型,为什么编译器不能计算出M(dynamic arg)的编译时类型?

根据规范§7.6.5:

如果以下至少一项成立,则调用表达式是动态绑定的(§7.2.2):

  • 主表达式的编译时类型为dynamic。

  • 可选参数列表中至少有一个参数的编译时类型为dynamic,而主表达式没有委托类型。

对于来说

class Foo {
    public int M(string s) { return 0; }
    public string M(int s) { return String.Empty; }
}

编译器无法计算的编译时类型

dynamic d = // dynamic
var x = new Foo().M(d);

因为它直到运行时才知道调用了CCD_ 4的哪个重载。

然而,如果M只有一个重载,或者M的所有重载都返回相同的类型,为什么编译器不能计算出编译时类型?

我想了解为什么规范不允许编译器在编译时静态地键入这些表达式。

为什么即使只有一个可能的返回类型,方法调用表达式也具有dynamic类型

更新:这个问题是我2012年10月22日博客的主题。谢谢你的提问!


如果M只有一个重载,或者M的所有重载都有相同的返回类型,为什么编译器不能计算出M(dynamic_expression)的编译类型?

编译器可以计算出编译时类型;编译时类型是动态,编译器成功地计算出了这一点。

我想你想问的问题是:

为什么M(dynamic_expression)的编译时类型总是动态的,即使在罕见且不太可能的情况下,您对方法M进行完全不必要的动态调用,而无论参数类型如何,都会选择该方法?

当你用这样的措辞表达问题时,它有点自我回答。:-)

原因一:

你设想的情况很少;为了使编译器能够进行您所描述的那种推断,必须知道足够的信息,以便编译器能够对表达式进行几乎完全的静态类型分析。但是,如果您处于这种情况,那么为什么首先要使用动态呢?你最好简单地说:

object d = whatever;
Foo foo = new Foo();
int x = (d is string) ? foo.M((string)d) : foo((int)d);

显然,如果M只有一个重载,那么就更容易了:将对象强制转换为所需的类型。如果它在运行时失败是因为强制转换不好,那么dynamic也会失败!

在这类场景中,根本不需要首先使用动态,那么我们为什么要在编译器中做大量昂贵而困难的类型推理工作,以启用我们不希望您首先使用动态的场景呢?

原因二:

假设我们确实说过,如果方法组静态已知包含一个方法,那么重载解析具有非常特殊的规则。太棒了现在,我们为语言添加了一种新的脆弱性。现在添加一个新的重载会将调用的返回类型更改为完全不同的类型——这种类型不仅会导致动态语义,还会框值类型。但是等等,情况越来越糟!

// Foo corporation:
class B
{
}
// Bar corporation:
class D : B
{
    public int M(int x) { return x; }
}
// Baz corporation:
dynamic dyn = whatever;
D d = new D();
var q = d.M(dyn);

让我们假设我们实现了您的特性request,并根据您的逻辑推断q是int。现在Foo公司增加:

class B
{
    public string M(string x) { return x; }
}

当Baz公司重新编译他们的代码时,突然q的类型悄悄地变成了动态的,因为在编译时我们不知道dyn不是字符串。这是一个奇怪的和静态分析中的意外变化!为什么第三方要将新方法添加到基类中,导致局部变量的类型在一个完全不同的类中以完全不同的方法更改,该类是在另一家公司编写的,该公司甚至不直接使用B,而是仅通过D?

这是脆性基类问题的一种新形式,我们试图在C#中最小化脆性基类问题。

或者,如果Foo corp说:

class B
{
    protected string M(string x) { return x; }
}

按照你的逻辑

var q = d.M(dyn);

当上面的代码在继承自D的类型的外部时,给q类型int,但

var q = this.M(dyn);

当内部的是从D继承的类型时,将q的类型指定为动态类型!作为一名开发人员,我会觉得这非常令人惊讶。

原因三:

C#中已经有太多的聪明了。我们的目标不是构建一个逻辑引擎,它可以在给定特定程序的情况下,对所有可能的值进行所有可能的类型限制。我们更喜欢有通用的、可理解的、易于理解的规则,这些规则可以很容易地写下来并在没有错误的情况下实现。该规范已经有八百页长,编写一个没有bug的编译器非常困难。我们不要让它变得更难。更不用说测试所有这些疯狂案例的费用了。

原因四:

此外:该语言为您提供了许多使用静态类型分析器的机会。如果您使用的是dynamic,则是特别要求分析器将其操作推迟到运行时。使用"在编译时停止执行静态类型分析"功能会导致静态类型分析在编译时不能很好地工作,这并不奇怪。

dynamic功能的早期设计支持这样的功能。编译器仍然会进行静态重载解析,并引入了一个"幻影重载",仅在必要时表示动态重载解析

  • 博客文章介绍幻影方法
  • 幻影方法的详细信息

正如您在第二篇文章中看到的,这种方法引入了很多复杂性(第二篇讨论了如何修改类型推理以使该方法发挥作用)。对于C#团队决定在涉及dynamic时始终使用动态过载分辨率这一更简单的想法,我并不感到惊讶。

然而,如果M只有一个重载,或者M的所有重载都返回相同的类型,为什么编译器不能计算出编译时的类型?

编译器可能会这样做,但语言团队决定不让它以这种方式工作。

dynamic的全部目的是让所有使用动态执行的表达式"它们的解析将推迟到程序运行时"(C#规范,4.2.3)。编译器明确不执行动态表达式的静态绑定(这是获得此处所需行为所必需的)。

如果只有一个绑定选项,则回退到静态绑定将迫使编译器检查这种情况-这是未添加的。至于语言团队不想这样做的原因,我怀疑Eric Lippert的回应适用于此:

我总是被问到"为什么C#不实现特性X?"。答案总是一样的:因为从来没有人设计、指定、实现、测试、记录和交付过该功能。

我认为能够静态确定动态方法解析的唯一可能返回类型的情况非常狭窄,如果C#编译器这样做,而不是具有全面的行为,则会更加混乱和不一致。

即使在您的示例中,如果Foo是另一个dll的一部分,Fao在运行时也可能是绑定重定向的更新版本,并具有其他返回类型的M,那么编译器可能猜错了,因为运行时解析将返回不同的类型。

如果FooIDynamicMetaObjectProviderd可能与任何静态参数都不匹配,因此它将依赖于可能返回不同类型的动态行为。