Grokay DLR

译者序

这更多是免费重述,而不是翻译。 在本文中,我仅包括原始文档中与DLR内部机制直接相关或解释重要思想的那些部分。 注释将放在方括号中。

许多.NET开发人员都听说过动态语言运行时(DLR),但对此一无所知。 用C#或Visual Basic之类的语言编写的开发人员避免使用动态类型的语言,因为担心历史上与可伸缩性相关的问题。 他们还担心Python或Ruby之类的语言在编译时不会执行类型检查,这可能导致难以找到和修复的运行时错误。 这些是有充分根据的担心,这些原因也许可以解释为什么DLR甚至在正式发布两年后仍不受到大多数.NET开发人员的欢迎(该文章已经很老了,但是此后没有发生任何变化) 。 毕竟,任何名称中包含词“ 动态”和“ 语言”的 .NET 运行时都应严格设计为支持Python等语言,对吗?

慢点 虽然DLR确实旨在支持.NET Framework中Iron和Python的Iron实现,但其体系结构提供了更深的抽象。



在后台,DLR提供了一组丰富的接口用于进程间通信[Inter-Process Communication(IPC)]。 多年来,开发人员已经看到许多用于在应用程序之间进行交互的Microsoft工具:DDE,DCOM,ActiveX,.Net Remoting,WCF,OData。 此列表可能会持续很长时间。 这几乎是无休止的首字母缩写游行,每种缩写都代表一项技术,该技术有望在今年比以前更轻松地交换数据或调用远程代码。

语言语言


我第一次听到吉姆·胡根宁(Jim Hugunin)谈论DLR时,他的演讲使我感到惊讶。 Jim为称为Jython的Java虚拟机(JVM)创建了Python实现。 演出开始前不久,他加入了Microsoft,为.NET创建IronPython。 基于他的背景,我希望他专注于语言,但是,吉姆几乎一直都在谈论诸如表达式树,动态调用分配和调用缓存机制之类的深奥的东西。 Jim描述了一组运行时编译服务,该服务允许任何两种语言相互交互而几乎不损失性能。

在演讲中,我写下了一个听到吉姆重提DLR体系结构时浮现在脑海的术语:“ 语言的语言 ”。 四年后,这个昵称仍然非常准确地代表了DLR。 但是,在获得了现实世界的使用经验之后,我意识到DLR不仅与语言兼容性有关。 由于C#和Visual Basic支持动态类型,DLR可以充当从我们喜欢的.NET语言到任何远程系统中的数据和代码的网关,而无论后者使用哪种设备或软件。



为了理解DLR(IPC语言中的一种集成机制)背后的思想,让我们从一个与动态编程无关的示例开始。 想象一下两个计算机系统:一个叫做启动器,另一个是目标系统。 启动器需要在目标系统上执行foo函数,并向其中传递一组特定的参数,并获得结果。 发现目标系统后,启动器必须以她可以理解的格式提供执行功能所需的所有信息。 至少,此信息将包括函数的名称和传递的参数。 解压缩请求并验证参数后,目标系统将执行foo函数。 之后,它应该打包结果,包括在执行过程中发生的所有错误,然后将其发送回启动器。 最后,发起者应该能够解压结果并通知目标。 这种请求-响应模式非常普遍,并且在较高级别上描述了几乎所有IPC机制的操作。

动态元对象


为了了解DLR如何实现所呈现的模式,让我们看一下DLR的核心类之一: DynamicMetaObject 。 我们首先探讨这种类型的十二种关键方法中的三种:

  1. BindCreateInstance-创建或激活对象
  2. BindInvokeMember-调用封装的方法
  3. BindInvoke-对象执行(作为函数)

当需要在远程系统上执行方法时,首先需要创建该类型的实例。 当然,并非所有系统都是面向对象的,因此术语“实例”可以是一个隐喻。 实际上,我们需要的服务可以实现为对象池或单例,以便可以将“激活”或“连接”与“实例”具有相同的权利。

其他框架遵循相同的模式。 例如,COM提供了用于创建对象的CoCreateInstance函数。 在.NET Remoting中,可以使用System.Activator类中的CreateInstance方法。 DLR DynamicMetaObject提供了BindCreateInstance用于类似目的。

使用BindCreateInstance方法后,创建的内容可以是公开几种方法的类型。 BindInvokeMember元对象方法用于绑定可以调用函数的操作。 在上图中,可以将字符串foo作为参数传递,以向活页夹指示应调用具有该名称的方法。 另外还包括有关参数数量,它们的名称和一个特殊标志的信息,该标志向活页夹指示在搜索合适的命名元素时是否可以忽略大小写。 毕竟,并非所有语言都区分大小写。

当从BindCreateInstance返回的内容只是一个函数(或委托)时,将使用BindInvoke方法。 为了弄清楚图片,让我们看一下下面的一小段动态代码:

delegate void IntWriter(int n); void Main() { dynamic Write = new IntWriter(Console.WriteLine); Write(5); } 

此代码不是将数字5打印到控制台的最佳方法。 一个好的开发人员永远不会使用任何浪费的东西。 但是,此代码说明了动态变量的使用,其值是可以用作函数的委托。 如果委托类型实现IDynamicMetaObjectProvider接口,则将使用DynamicMetaObject中BindInvoke方法将操作绑定到实际工作。 这是因为编译器认识到动态Write对象在语法上被用作函数。 现在考虑另一段代码,以了解编译器何时生成BindInvokeMember

 class Writer : IDynamicMetaObjectProvider { public void Write(int n) { Console.WriteLine(n); } //    } void Main() { dynamic Writer = new Writer(); Writer.Write(7); } 

在这个小示例中,我将省略该接口的实现,因为它将花费大量代码来正确演示这一点。 在这个简短的示例中,我们仅用几行代码即可实现一个动态元对象。

要理解的重要一点是,编译器认识到Writer.Write(7)是元素访问操作。 我们通常将C#中通常称为“点运算符”的形式正式称为“类型成员访问运算符”。 在这种情况下,由编译器生成的DLR代码最终将调用BindInvokeMember ,它将把Write字符串和参数编号7传递到其中可以进行调用的操作中。 简而言之, BindInvoke用于将动态对象作为函数调用,而BindInvokeMember用于将方法作为动态对象的元素调用。

通过DynamicMetaObject访问属性


从以上示例可以看出,编译器使用语言语法来确定应执行哪些DLR绑定操作。 如果使用Visual Basic处理动态对象,则将使用其语义。 当然,不仅需要访问方法,还需要访问运算符(点)。 您可以使用它来访问属性。 DLR元对象提供了三种访问动态对象属性的方法:

  1. BindGetMember-获取属性值
  2. BindSetMember-设置属性值
  3. BindDeleteMember-删除项目

BindGetMemberBindSetMember的目的应该很明显。 尤其是现在您已经知道它们与.NET如何使用属性相关联。 当编译器计算动态对象的get (“读取”)属性时,它将使用对BindGetMember的调用。 编译器计算set(“ record”)时,将使用BindSetMember

将对象表示为数组


一些类是其他类型实例的容器。 DLR知道如何处理此类情况。 每个“面向数组”的元对象方法都有一个“索引”后缀:

  1. BindGetIndex-通过索引获取值
  2. BindSetIndex-按索引设置值
  3. BindDeleteIndex-按索引删除值

要了解如何使用BindGetIndexBindSetIndex ,请想象一个JavaBridge包装器该类可以加载Java类的文件,并允许您从.NET代码中轻松使用它们。 这样的包装器可用于加载包含一些ORM代码的Customer Java类。 DLR元对象可用于以经典C#样式从.NET调用此ORM代码。 下面是示例代码,显示了JavaBridge如何在实践中工作:

 JavaBridge java = new JavaBridge(); dynamic customers = java.Load("Customer.class"); dynamic Jason = customers["Bock"]; Jason.Balance = 17.34; customers["Wagner"] = new Customer("Bill"); 

由于第三和第五行按索引([])使用访问运算符,因此在处理从JavaBridge返回的元对象时,编译器会识别此字符并使用BindGetIndexBindSetIndex方法 。 可以理解,在返回的对象上实现这些方法将通过Java远程方法调用(RMI)从JVM请求执行该方法。 在这种情况下,DLR充当C#和具有静态类型的另一种语言之间的桥梁。 希望这阐明了为什么我称DLR为“语言”。

BindDeleteIndex一样, BindDeleteMember方法也不适合在具有静态类型的语言(如C#和Visual Basic)中使用,因为它们不支持该概念本身。 但是,如果对您有用,您可以同意考虑“删除”用语言手段表达的某些操作。 例如,您可以将BindDeleteMember实现为按索引使元素为空。

变换和运算符


最后一组DLR元对象方法是关于处理运算符和转换的。

  1. BindConvert-将对象转换为另一种类型
  2. BindBinaryOperation-在两个操作数上使用二进制运算符
  3. BindUnaryOperation-在一个操作数上使用一元运算符

当编译器意识到该对象需要转换为另一种已知类型时,将使用BindConvert方法。 当将动态调用的结果分配给具有静态类型的变量时,就会发生隐式转换。 例如,在下面的C#示例中,分配变量y导致对BindConvert的隐式调用:

 dynamic x = 13; int y = x + 11; 

当遇到算术运算(“ +”)或增量(“ ++”)时,始终使用BindBinaryOperationBindUnaryOperation方法 。 在上面的示例中,将动态变量x添加到常量11将调用BindBinaryOperation方法。 记住这个小例子,我们将在下一节中使用它来敲打另一个称为CallSite的关键DLR类。

使用CallSite进行动态调度


如果您对DLR的介绍不只是使用dynamic关键字,那么您可能永远不会知道.NET Framework中CallSite的存在。 这种普通的类型(正式称为CallSite < T > )位于System.Runtime.CompilerServices命名空间中 。 这是元编程的“动力源”:它充满了各种优化方法,这些方法使动态.NET代码变得快速高效。 我将在文章结尾提到CallSite < T >性能方面。

CallSite在动态.NET代码中所做的大部分工作都涉及在运行时中生成和编译代码。 请务必注意, CallSite < T >类位于包含单词“ Runtime ”和“ CompilerServices ”的名称空间中。 如果DLR是“语言的语言”,则CallSite < T >是其最重要的语法结构之一。 让我们再次看一下上一节中的示例,以了解CallSite以及编译器如何将它们嵌入到您的代码中。

 dynamic x = 13; int y = x + 11; 

如您所知,将调用BindBinaryOperaionBindConvert方法来执行此代码。 我没有向您展示由编译器生成的反汇编的MSIL代码的详细列表,而是绘制了一个图表:



请记住,编译器使用语言语法来确定要执行的动态类型方法。 在我们的示例中,执行了两个操作:将变量x添加到数字( Site2 )并将结果强制转换为int( Site1 )。 这些操作中的每一个都转换为CallSite,并存储在一个特殊的容器中。 正如您在图中所看到的,CallSites是按照相反的顺序创建的,但是以正确的方式被调用。

在该图中,您可以看到在操作“创建CallSite1”和“创建CallSite2”之前紧接着调用了元对象方法BindConvertBindBinaryOperation 。 但是,绑定操作仅在最后执行。 我希望可视化可以帮助您理解绑定方法和调用绑定方法是DLR上下文中的不同操作。 而且,绑定仅发生一次,而调用根据需要进行多次,从而重用已经初始化的CallSites来优化性能。

遵循简单的方法


在DLR的核心部分,表达式树用于生成与上述十二种绑定方法相关的函数。 许多开发人员经常使用LINQ面对表达式树,但是只有少数开发者具有足够的经验来完全实现IDynamicMetaObjectProvider合同。 幸运的是,.NET Framework包含一个称为DynamicObject的基类,可以处理大部分工作。

要创建自己的动态类,您要做的就是从DynamicObject继承并实现以下十二种方法:

  1. TryCreateInstance
  2. TryInvokeMember
  3. Tryinvoke
  4. TryGetMember
  5. TrySetMember
  6. TryDeleteMember
  7. TryGetIndex
  8. TrySetIndex
  9. TryDeleteIndex
  10. 尝试转换
  11. TryBinaryOperation
  12. TryUnaryOperation

方法名称看起来熟悉吗? 您必须这样做,因为您刚刚完成了Abstract DynamicMetaObject类的元素的研究,其中包括BindCreateInstanceBindInvoke之类的方法。 DynamicMetaObject类为IDynamicMetaObjectProvider提供了一个实现,该实现从其唯一方法返回DynamicMetaObject 。 与元对象的基本实现相关的操作仅将其调用委托给DynamicObject实例上以“ Try”开头的方法。 您需要做的就是在从DynamicObject继承的类中重载TryGetMemberTrySetMember之类的方法,而meta对象将承担表达式树的所有工作。

快取


[您可以在我之前关于DLR的文章中阅读有关缓存的更多信息]

为开发人员使用动态语言时,最大的担忧是性能。 DLR采取非常规措施消除了这些经验。 我简要提到了CallSite < T >驻留在名为System.Runtime.CompilerServices的命名空间中的事实。 在同一名称空间中还有其他几个提供多级缓存的类。 使用这些类型,DLR实现了三个主要的缓存级别以加快动态操作:

  1. 全局缓存
  2. 本地缓存
  3. 多态委托缓存

使用缓存是为了避免不必要的资源浪费,从而无法为特定的CallSite创建绑定。 如果将两个string类型的对象传递给返回int的动态方法,则全局或本地缓存将保存结果绑定。 这将大大简化后续的呼叫。

位于CallSite自身内部的委托缓存称为多态的,因为这些委托可以采用不同的形式,具体取决于执行了哪些动态代码以及使用了来自其他缓存的规则来生成它们。 委托缓存有时也称为内联缓存。 使用此术语的原因是,与其他任何.NET代码一样,由DLR及其绑定程序生成的表达式将转换为通过JIT编译传递的MSIL代码。 运行时的编译与程序的“正常”执行同时进行。 显然,在程序执行期间将动态代码动态转换为已编译的MSIL代码会极大地影响应用程序性能,因此缓存机制至关重要。

Source: https://habr.com/ru/post/zh-CN469075/


All Articles