5

你能给我一些关于 C# 中动态类型限制的原因吗?我在“Pro C# 2010 and the .NET 4 platform”中读到了它们。这是一段摘录(如果在这里引用书籍是非法的,请告诉我,我将删除摘录):

虽然可以使用 dynamic 关键字定义很多东西,但它的使用存在一些限制。虽然它们不是显示停止器,但要知道动态数据项在调用方法时不能使用 lambda 表达式或 C# 匿名方法。例如,以下代码总是会导致错误,即使目标方法确实接受了一个委托参数,该委托参数接受一个字符串值并返回 void。

dynamic a = GetDynamicObject(); 
// Error!  Methods on dynamic data can’t use lambdas! 
a.Method(arg => Console.WriteLine(arg));

为了规避这个限制,你需要直接使用底层委托,使用第 11 章中描述的技术(匿名方法和 lambda 表达式等)。另一个限制是动态数据点无法理解任何扩展方法(参见第 12 章)。不幸的是,这也将包括来自 LINQ API 的任何扩展方法。因此,使用 dynamic 关键字声明的变量在 LINQ to Objects 和其他 LINQ 技术中的用途非常有限:

dynamic a = GetDynamicObject(); 
// Error!  Dynamic data can’t find the Select() extension method! 
var data = from d in a select d;

提前致谢。

4

3 回答 3

15

托马斯的猜想很不错。他对扩展方法的推理是正确的。基本上,为了使扩展方法工作,我们需要调用站点在运行时以某种方式知道在编译时使用指令是什么。我们根本没有足够的时间或预算来开发一个可以将这些信息持久保存到呼叫站点的系统。

对于 lambdas,情况实际上比确定 lambda 是要进入表达式树还是委托的简单问题要复杂得多。考虑以下:

d.M(123)

其中 d 是动态类型的表达式。*应该在运行时将什么对象作为参数传递给调用站点“M”?显然,我们将 123 框起来并通过它。然后运行时绑定器中的重载解析算法查看 d 的运行时类型和 int 123 的编译时类型并使用它。

现在如果它是

d.M(x=>x.Foo())

现在我们应该传递什么对象作为参数?我们无法表示“一个变量的 lambda 方法,它调用一个名为 Foo 的未知函数,无论 x 的类型是什么”。

假设我们想要实现这个特性:我们必须实现什么?首先,我们需要一种表示未绑定 lambda的方法。表达式树在设计上仅用于表示已知所有类型和方法的 lambda。我们需要发明一种新的“无类型”表达式树。然后我们需要在运行时绑定器中实现 lambda 绑定的所有规则。

考虑最后一点。Lambda 可以包含语句实现此功能需要运行时绑定器包含C# 中每个可能语句的整个语义分析器

这超出了我们的预算数量级。如果我们想要实现该功能,我们今天仍将致力于 C# 4。

不幸的是,这意味着 LINQ 不能很好地处理动态,因为 LINQ 当然到处都使用无类型的 lambda。希望在某个假设的 C# 未来版本中,我们将拥有功能更全面的运行时绑定器,并能够对未绑定的 lambda 进行同音表示。但如果我是你,我不会屏住呼吸等待。

更新:评论要求澄清有关语义分析器的观点。

考虑以下重载:

class C {
  public void M(Func<IDisposable, int> f) { ... }
  public void M(Func<int, int> f) { ... }
  ...
}

和一个电话

d.M(x=> { using(x) { return 123; } });

假设 d 是编译时类型 dynamic 和运行时类型 C。运行时绑定器必须做什么?

运行时绑定器必须在运行时确定表达式x=>{...}是否可转换为 M 的每个重载中的每个委托类型。

为此,运行时绑定器必须能够确定第二个重载不适用。如果它适用,那么您可以将 int 作为 using 语句的参数,但 using 语句的参数必须是一次性的。这意味着运行时绑定器必须知道 using 语句的所有规则,并且能够正确报告 using 语句的任何可能使用是合法的还是非法的

显然,这不仅限于 using 语句。运行时绑定器必须了解所有 C# 的所有规则,以便确定给定语句 lambda 是否可转换为给定委托类型。

我们没有时间编写运行时绑定器,它本质上是一个全新的 C# 编译器,它生成 DLR 树而不是 IL。通过不允许 lambda,我们只需要编写一个运行时绑定器,它知道如何绑定方法调用、算术表达式和其他一些简单类型的调用站点。允许 lambdas 使运行时绑定问题的实现、测试和维护成本提高了数十或数百倍。

于 2010-08-28T01:31:10.523 回答
8

Lambdas:我认为不支持 lambdas 作为动态对象的参数的一个原因是编译器不知道是将 lambdas 编译为委托还是表达式树。

当您使用 lambda 时,编译器会根据目标参数或变量的类型来决定。当它是Func<...>(或其他委托)时,它将 lambda 编译为可执行委托。当目标是Expression<...>它时,它将 lambda 编译为表达式树。

现在,当你有一个dynamic类型时,你不知道参数是委托还是表达式,所以编译器无法决定要做什么!

扩展方法:我认为这里的原因是在运行时找到扩展方法会非常困难(而且可能效率低下)。首先,运行时需要知道使用using. 然后它需要搜索所有加载的程序集中的所有类,过滤那些可访问的类(按命名空间),然后搜索那些扩展方法......

于 2010-08-28T00:58:27.533 回答
2

埃里克(和托马斯)说得很好,但这就是我的看法。

这个 C# 语句

a.Method(arg => Console.WriteLine(arg)); 

没有很多上下文就没有意义。Lambda 表达式本身没有类型,而是可以转换为delegate(或Expression)类型。因此,收集含义的唯一方法是提供一些上下文,强制将 lambda 转换为特定的委托类型。该上下文通常是(如本例所示)重载决议;给定 的类型a,以及该类型的可用重载Method(包括扩展成员),我们可能会放置一些赋予 lambda 含义的上下文。

如果没有上下文来产生含义,您最终不得不捆绑有关 lambda 的各种信息,希望在运行时以某种方式绑定未知数。(你可能会生成什么 IL?)

与此形成鲜明对比的是,您将特定的委托类型放在那里,

a.Method(new Action<int>(arg => Console.WriteLine(arg))); 

卡赞!事情变得简单了。无论 lambda 中的代码是什么,我们现在都知道它具有什么类型,这意味着我们可以像编译任何方法体一样编译 IL(例如,我们现在知道Console.WriteLine我们正在调用的众多重载中的哪一个) . 并且该代码有一个特定的类型 ( Action<int>),这意味着运行时绑定器很容易查看是否a有一个Method接受该类型参数的类型。

在 C# 中,裸露的 lambda 几乎没有意义。C# lambda 需要静态上下文来赋予它们意义并排除由许多可能的强制和重载引起的歧义。一个典型的程序很容易提供这个上下文,但是这个dynamic案例缺少这个重要的上下文。

于 2010-08-28T06:38:52.940 回答