c#

位置:IT落伍者 >> c# >> 浏览文章

C# 动态编程新特性与DLR剖析


发布日期:2018年11月30日
 
C# 动态编程新特性与DLR剖析

近几年来在TIOBE公司每个月发布的编程语言排行榜[]中总是能挤进前而在近年的编程语言排行榜中C#总体上呈现上升的趋势C#能取得这样的成绩有很多因素在起作用其中它在语言特性上的锐意进取让人印象深刻(图

C#各版本的创新点

年发布的C# 最大的创新点是拥有了动态编程语言的特性

动态编程语言的中兴

动态编程语言并非什么新鲜事物早在面向对象编程语言成为主流之前人们就已经使用动态编程语言来开发了即使在C#等面向对象编程语言繁荣兴旺大行于世的年代动态编程语言也在悄悄地攻城掠地占据了相当的开发领域比如 业已成为客户端事实上的主流语言

最近这几年动态编程语言变得日益流行比如PythonRuby都非常活跃使用者众多

这里有一个问题为什么我们需要在开发中应用动态编程语言?与C#和Java这类已经非常成熟且功能强大的静态类型编程语言相比动态编程语言有何优势?

简单地说使用动态编程语言开发拥有以下的特性

)支持REPL(Readevaluateprint Loop读入à执行à输出循环迭代)的开发模式整个过程简洁明了直指问题的核心

举个简单的例子所示为使用IronPython[]编程计算++……+的屏幕截图我们可以快速地输入一段完成累加求和的代码然后马上就可以看到结果

使用IronPython编程

如果使用开发就麻烦多了您得先用Visual Studio创建一个项目然后向其中添加一个类在类中写一个方法完成求和的功能再编写调用这一方法的代码编译排错最后才能得到所需的结果……

很明显对于那些短小的工作任务而言动态编程语言所具备的这种REPL开发模式具有很大的吸引力

)扩展方便用户可以随时对代码进行调整需要什么功能直接往动态对象上就是了不要时又可以移除它们而且这种修改可以马上生效并不需要像C#那样必须先修改类型的定义和声明编译之后新方法才可用

换句话说使用动态语言编程不需要重量级的OOAD整个开发过程迭代迅速而从不拖泥带水

)动态编程语言的类型解析是在运行时完成的可以省去许多不必要的类型转换代码因此与静态编程语相比动态编程语言写的代码往往更紧凑量更少

动态编程语言主要的弱点有两个

)代码中的许多错误要等到运行时才能发现而且需要特定的运行环境支持对其进行测试不太方便也不支持许多用于提升代码质量的各种工具因此不太适合于开发规模较大的包容复杂处理逻辑的应用系统

)与静态编程语言相比动态编程语言编写的程序性能较低不过随着计算机软技术的不断进步比如多核的广泛应用动态编程语言引擎和运行环境不断地优化动态编程语言编写的程序性能在不断地提升在特定的应用场景下甚至可以逼近静态语言编写的程序

拥抱动态编程特性的

为了让C#Visual Basic等编程语言能具备动态编程语言的特性NET 引入了一个DLR(Dynamic Language Runtime动态语言运行时)(图

DLR动态语言运行时

DLR运行于CLR之上提供了一个动态语言的运行环境从而允许PythonRuby等动态语言编写的程序在NET平台上运行同时现有的NET静态类型编程语言比如C#和Visual Basic也可以利用DLR而拥有一些动态编程语言的特性

)使用C# 编写动态的代码

C# 新增了一个dynamic关键字可以用它来编写动态的代码

例如以下代码创建了一个ExpandoObject对象(注意必须定义为dynamic)

dynamic dynamicObj = new ExpandoObject();

这一对象的奇特之处在于我们可以随时给它增加新成员

dynamicObjValue=;//添加字段

dynamicObjIncrement=newAction(()=>dynamicObjValue++);//添加方法

这些动态添加的成员与普通的类成员用法一样

for(inti=;i<;i++)

dynamicObjIncrement();//调用方法

ConsoleWriteLine(dynamicObjValue={}dynamicObjValue);//访问字段

ExpandoObject对象实现了IDictionary<string object>接口可看成是一个字典对象所有动态添加的成员都是这个字典对象中的元素这意味我们不仅可以添加新成员还可以随时移除不再需要的成员

//移除Increment方法

(dynamicObjasIDictionary<stringobject>)Remove(Increment);

方法移除之后再尝试访问此方法将引发RuntimeBinderException异常

)使用dynamic关键字简化与COM组件交互的代码

要在这个托管世界里调用非托管世界中的COM组件我们必须通过 互操作程序集(Interop Assembly)作为桥梁互操作程序集定义了CLR类型与COM类型之间的对应关系

只要给NET项目添加对互操作程序集的引用就可以在NET应用程序中创建这一程序集所包容的各种类型的实例(即COM包装器对象)对这些对象的方法调用(或对其属性的存取)将会被转发给COM组件

以调用Word为例之前您可能经常需要编写这样的代码

Objectwordapp=newWordApplication();//创建Word对象

ObjectfileName=MyDocdocx;//指定Word文档

Objectargu=SystemReflectionMissingValue;

WordDocumentdoc=wordappDocumentsOpen(reffileNamerefargu

refargurefargurefargurefargurefargurefargu

refargurefargurefargurefargurefargurefargu

refargurefargu);

上述对Open()方法的调用语句只能用恐怖一词来形容其原因是Word组件中的Open()方法定义了太多的参数

使用dynamic关键字配合从Visual Basic中学来的命名参数与可选参数这两个新语法特性可以写出更简洁的代码

dynamicwordapp=newWordApplication();

dynamicdoc=wordappDocumentsOpen(FileName:MyDocdocx);

上述代码中省去了用不着的参数并且可以去掉参数前的ref关键字

当上述代码运行时DLR会使用反射技术将dynamic表达式绑定(bind)到COM互操作程序集中所包容的WordApplication代理对象

)C# 动态编程技术内幕

C#中所定义的dynamic变量可以引用以下类型的对象

l传统的静态的CLR对象

lCOM包装器对象前面已经介绍了这方面的内容

l实现了IDynamicMetaObjectProvider接口的动态对象ExpandoObject就是这种类型对象的实例

l基于DLR实现的动态语言(比如IronRuby和IronPython)所创建的对象

从程序员角度来看所有这四种对象都是一样的都可用一个dynamic变量引用之而DLR在程序运行时动态地将方法调用和字段存取请求绑定到真正的对象上

dynamic的功能是由DLR所支撑的是C#编译器与DLR分工合作的成果

请看以下示例代码

dynamicd=;

d++;

C#编译器在处理上述代码时它并不去检查变量d是否可以支持自增操作而是为其创建了一个CallSite<T>对象(<>p__Site

privatestaticclass<Main>o__SiteContainer{

publicstaticCallSite<Func<CallSiteobjectobject>><>p__Site;

}

中文MSDN将CallSite<T>译为动态(调用)站点它是DLR中的核心组件之一

动态站点对象通过CallSite<T>Create()方法创建 C#编译器会为其指定一个派生自CallSiteBinder的对象(称为动态站点绑定对象)作为其参数

动态站点绑定对象是与具体语言相关的比如IronPython和C#都有各自的动态站点绑定对象

动态站点绑定对象的主要工作是将代码中的动态表达式(本例中为d++)转换为一棵抽象语法树(ASTAbstract Syntax Tree)这棵语法树被称为DLR Tree是在 所引入的LINQ表达式树的基础上扩充而来的因此有时又称其为表达式树(Expression Tree)

DLR在内部调用此表达式树的Compile()方法生成IL指令得到一个可以被CLR所执行的委托(在本例中其类型就是Func<CallSite object object>)

动态调用站点对象(本例中为<>p__Site)有一个Target属性它负责引用这一生成好的委托

委托生成之后动态表达式的执行就体现为委托的执行其实参由编译器直接写死在IL代码中

简化的代码示意如下(通过Reflector得到为便于阅读修改了变量名)

objectd=;

objectCS$$=d;

if(<>p__Site==null)

<>p__Site=CallSite<Func<CallSiteobjectobject>>Create(……);

d=<>p__SiteTarget(<>p__SiteCS$$);

上述类型推断方法绑定及IL代码生成的工作都是在程序运行时完成的

)动态代码很慢吗?

动态编程语言易学易用代码紧凑开发灵活但性能则一直是它的软肋为了提升性能DLR设计了一个三级缓存策略

动态站点绑定对象会为动态调用表达式转换而成的语法树加上相应的测试条件(称为test构成一个规则(Rule)这个规则可以用于判断某个语法树是否可用于特定的动态调用表达式

举个例子请看以下这个动态表达式

d + d

如果在程序运行时d和d都是int类型的整数则DLR生成的规则为

if(disint&&disint)//测试条件

return(int)d+(int)d;//语法树

DLR通过检查规则中的测试条件就可以知道某个动态表达式是否可以使用此规则所包容的语法树

规则是DLR缓存的主要对象

前面介绍过的动态站点对象Target属性所引用的委托是第一级缓存它实现的处理逻辑是这样的

//当前处理规则属于第级缓存

if(disint&&disint)//测试条件

return(int)d+(int)d;//满足测试条件直接返回一个表达式树

//未命中则在第级缓存中查找如果找到了用找到的结果更新第级缓存

returnsiteUpdate(sitedd);

如果级缓存中都没有命中的规则则此动态站点所关联的调用站点绑定对象会尝试创建一个新的规则如果创建新规则失败则由当前编程语言(比如)所提供的默认调用站点绑定对象决定如何处理通常的作法是抛出一个异常

当前版本的DLR第级缓存了条规则级则缓存了条规则

由于DLR自身设计了一个规则缓存系统又充分利用了CLR所提供的JIT缓存(因为所有动态调用代码最终都会转换为CLR可以执行的IL指令而CLR可以缓存这些代码)使得动态代码仅仅在第一次执行时性能较差后续的连续调用其性能可以逼近静态代码

C# 与动态语言的集成

由于几乎所有的编程语言都可以使用抽象语法树来表达因此在理论上DLR支持无限多种编程语言间的互操作在当前版本中可以实现C#/Visual Basic与IronPython和IronRuby的互操作相信很快会出现其他动态编程语言的DLR实现

一个有趣的地方是当前基于DLR实现的动态编程语言都以Iron开头比如IronRuby和IronPythonIronPython的设计者DLR的架构设计师Jim Hugunin曾经在微软PDC 大会上解释说主要是为了避免起一个PythonPython for NET之类微软味十足的名字才有了IronPython他强调Iron系列动态语言将严格遵循动态语言自身的标准和规范尊重这些动态语言已有的历史和积累不会引入一些仅限于NET平台的新语言特性并且这些语言的NET实现保持开源与此同时Jim Hugunin指出 Iron系列语言能很好地与NET现有类库编程语言和工具集成并且能嵌入NET宿主程序中

)动态对象通讯协议

由于各种动态编程语言之间的特性相差极大实现各语言间的互操作是个难题为此DLR采取了一个聪明的策略它不去尝试设计一个通用的类型系统(CLR就是这么干的)而是设计了一个通用的对象通讯协议规定所有需要互操作的动态对象必须实现IDynamicMetaObjectProvider接口此接口定义了一个GetMetaObject()方法接收一个语法树对象作为参数向外界返回一个动态元数据(DynamicMetaObject)对象

DynamicMetaObject GetMetaObject(Expression parameter);

DynamicMetaObject对象向外界提供了两个重要属性Restrictions引用一组测试条件Expression属性则引用一个语法树这两个属性组合起来就是可供动态站点对象缓存的规则(Rule)

DLR中的动态站点绑定对象(CallSiteBinder)获取了DynamicMetaObject对象之后它调用此对象所提供的各个方法创建规则动态站点对象(CallSite<T>)的Target属性引用它完成动态绑定的工作

)动态语言集成环境

为了方便地实现静态编程语言与各种动态编程语言间的相互集成DLR提供了一整套称为通用寄宿(Common Hosting)的组件其中包容ScriptRuntimeScriptScope等类型

下面我们以IronPython为例介绍如何在 开发的程序中集成动态编程语言代码

首先需要创建一个ScriptRuntime对象它是一个最顶层的对象用于在一个应用程序域中嵌入一个特定动态语言的运行环境

ScriptRuntime pythonRuntime = PythonCreateRuntime();

接着需要创建一个ScriptEngine对象它是动态语言代码的执行引擎

ScriptEngine engine = pythonRuntimeGetEngine(py);

ScriptScope对象类似于中的命名空间其中可以通过定义一些变量向动态代码传入数据比如下述代码将一个C# 创建的ExpandoObject对象传给Python代码

ScriptScopescope=pythonRuntimeCreateScope();

//C#创建动态对象

dynamicexpando=newExpandoObject();

expandoName=JinXuLiang;//动态添加一个字段

//让IronPython接收C#创建的Expando对象

scopeSetVariable(ExpandoObjectexpando);

stringpythonCode=printExpandoObjectName;

//IronPython引擎执行Python语句

engineCreateScriptSourceFromString(pythonCode)Execute(scope);

上述示例代码是直接执行Python代码在实际开发中更常见的是直接执行Python文件中的代码假设有一个Calculatorpy文件其中定义了一个Add函数

defAdd(ab):

returna+b

则以下C#代码可以直接执行之

ScriptRuntimepythonRuntime=PythonCreateRuntime();

dynamicpythonFile=pythonRuntimeUseFile(Calculatorpy);

ConsoleWriteLine(pythonFileAdd());

上述示例说明在DLR的支持之下可以让静态编程语言使用动态语言所开发的库反过来基于DLR实现的动态编程语言也能使用为静态语言所设计的库比如标准的基类库

这意味着两点

)我们现在可以将静态动态编程语言组合起来开发出一些具有高度交互性的应用程序使用静态编程语言搭建系统框架使用动态编程语言实现交互性这是一个很值得注意的应用领域

)将来会出现一些静态动态编程语言同时适用的库向实现无所不在的复用目标又前进了一步

Visual Studio 为新的NET编程语言F#提供了专门的项目模板但没有为IronPython和IronRuby之类动态语言的开发提供支持相信随着动态语言在NET平台之上的应用日趋广泛后继版本的Visual Studio会直接支持动态语言的开发

从C# ~所走过的路可以很清晰地看到它的发展轨迹得到这样的一个结论

未来的编程语言应该是多范式的具有高度的可组合性在一个项目或产品中组合多个编程语言使用多种编程范式会变得越来越普遍

我们可以推断C#的后继版本将会在此条道路上越走越远……

               

上一篇:微软预览Visual Studio新版Orcas新功能

下一篇:.NET Hash 简单实例