主要内容:
- DLR在面向服务设计的必要性
- dynamic类型
- DLR ScriptRuntime
- DynamicObject & ExpandoObject
1.DLR在面向服务设计中的必要性
题外前因话:商业业务环境的变更总是伴随着技术的变革。近来面向服务编程SOA的兴起是一种必然,涉及以下几个因素:
其一商业经营需要及时快速的调整;
其二在快速调整的同时,无需额外的开销,无需重构代码,重新发布平台;
其三商业平台的扩展性和稳定性;
其四商业平台维护费用;
SOA在业务平台整体上进行布局整合,发布服务契约,数据契约等等,那么涉及细颗粒度的操作,比如说商场需要实时对销货策略进行变更。1.客户类别不同打折销售,2.节假日不同打折,3.不同的商品类别折扣不同,4.促销商品折扣,5.销售额满多少,折扣多少等等,我们需要以后对销售策略进行实时维护,能新增,删除,修改。在当前我们可能通过App.config文件设置不同参数值来满足我们的业务需求,或者通过dll反射的方式来选择不同销售策略。但这些能灵活配置的有限,无法满足日益增长复杂业务逻辑的变化。对此我们需要这些业务需求能够“动态加载,动态注入”,最好是脚本语言,不需要进行第二次编译,现有的Framework能够自动解析执行。
C#仍是一种静态的类型化语言,无法完美支持动态编程。而Ruby,Python,JavaScript等天生在动态编程方面有绝对的优势。如果能加载第三方动态语言,并且自身具备和这些动态语言相同的某些动态功能。DLR(Dynamic Language Runtime)是添加到CLR的一系列服务,具有动态运行功能,并且允许加载第三方动态语言脚本,如Ruby和Python。DLR体系结构图如下:
2.新的关键字dynamic
dynamic是支持动态编程新的关键字,用此关键字声明object,编译器不会检查其类型安全,认定其任何操作都是安全有效的,只有再运行时才会判断,如果有错误,则会抛出RuntimeBinderException异常。这里要提到的是var关键字,var的对象是延迟确定的,当编译成IL时,其类型已经确定,且类型不可在运行时更改。dynamic类型可以在运行时根据需要转换为另外的类型。下表中描述他们的区别:
Dynamic | Var | |
编译器检查 | 不会检查 | 会检查 |
类型安全 | 不安全抛出RuntimeBinderException | 类型安全 |
类型确定 | 类型不确定,可多次转换(转换对象之间不必存在关系) | 类型延迟确定,可按照继承关系进行转换 |
下面用代码来展示下dynamic类型转换过程。可惜的是MS未对dynamic对象的方法提供智能感知(实际上对象在运行才能确定,也无从提供其方法,数据信息),感觉回归到了C时代。
01 | class Program |
02 | { |
03 | static void Main( string [] args) |
04 | { |
05 | dynamic dyn; |
06 |
07 | dyn = 10; |
08 | Console.WriteLine(dyn.GetType()); |
09 | Console.WriteLine(dyn); |
10 |
11 | dyn = "a string object" ; |
12 | Console.WriteLine(dyn.GetType()); |
13 | Console.WriteLine(dyn); |
14 |
15 | dyn = new User { Nickname = "loong" }; |
16 | Console.WriteLine(dyn.GetType()); |
17 | Console.WriteLine(dyn.GetWelcomeMsg()); |
18 | } |
19 | } |
20 |
21 | class User |
22 | { |
23 | public string Nickname { get ; set ; } |
24 |
25 | public string GetWelcomeMsg() |
26 | { |
27 | return string .Format( "Welcome {0}!" , this .Nickname); |
28 | } |
29 | } |
dynamic对象的类型是在运行时才根据当前对象来进行判断的,并且可以多次变更。这段代码执行结果如下:
good!那这样我们是不是可以说dynamic是万能的呢,实际上由于其对象类型不确定,也决定了不支持扩展方法,匿名方法(包含lambda表达式)也不能用做动态方法调用的参数。其次延伸开来,LINQ也与动态code无缘了。因为大多LINQ都是扩展方法。
C#是一种静态、类型化的语言,那么又是如何实现动态对象和方法的呢。我们先来看静态类型和动态类型在IL中有和区别。如下代码


1 class Program 2 { 3 static void Main(string[] args) 4 { 5 StaticUser staticUser = new StaticUser(); 6 //DynamicUser dynamicUser = new DynamicUser(); 7 8 Console.WriteLine(staticUser.Nickname); 9 //Console.WriteLine(dynamicUser.Nickname); 10 11 Console.ReadLine(); 12 } 13 } 14 15 class StaticUser 16 { 17 public string Nickname = "Loong"; 18 } 19 20 class DynamicUser 21 { 22 public dynamic Nickname = "Loong"; 23 }
我们来看下IL:
动态类型属性声明是
.field public object Nickname .custom instance void [System.Core]System.Runtime.CompilerServices.DynamicAttribute::.ctor() = ( 01 00 00 00 )
而静态类型属性声明
.field public string Nickname
动态类型属性是有一个object来代替目前声明的对象,之后用了System.Runtime.CompilerServices.DynamicAttribute。进而动态构造对象。这就是两者之间的差别。
我们再来比较下静态类型和动态类型在方法中执行情况:
首先是静态类型, IL_0001行调用newobj构造函数,IL_0008行获取该属性的value, 然后下一行输出。


1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint 4 // Code size 26 (0x1a) 5 .maxstack 1 6 .locals init ([0] class Loong.Dynamic.StaticUser staticUser) 7 IL_0000: nop 8 IL_0001: newobj instance void Loong.Dynamic.StaticUser::.ctor() 9 IL_0006: stloc.0 10 IL_0007: ldloc.0 11 IL_0008: ldfld string Loong.Dynamic.StaticUser::Nickname 12 IL_000d: call void [mscorlib]System.Console::WriteLine(string) 13 IL_0012: nop 14 IL_0013: call string [mscorlib]System.Console::ReadLine() 15 IL_0018: pop 16 IL_0019: ret 17 } // end of method Program::Main
动态类型,通过Microsoft.CSharp.RuntimeBinder初始化Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo C#动态操作执行信息,RuntimeBinderException处理 C# 运行时联编程序中的动态绑定时发生的错误等等。
其中有两个重要的类System.Runtime.CompilerServices.CallSite和System.Runtime.CompilerServices.CallSiteBinder.CallSite主要在运行期间查找动态对象归属类型,个人认为会和系统默认类型匹配, 继而再和自定义类型匹配。也就是说会扫描该应用程序域manged heap,查找该type。以确保该type有需要的执行的属性、方法等等。这个过程是非常耗时耗能的,MS也给出解决方案,CallSite这个类会缓存查找到的type信息,从而避免了重复查找。当CallSite执行完查找任务后,找到对应的type就调用CallSiteBinder()方法,产生表达树(这点和linq类似),表示将要执行的操作。如果没有找到该Type,则会抛出RuntimeBinderException。


1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint 4 // Code size 125 (0x7d) 5 .maxstack 8 6 .locals init ([0] class Loong.Dynamic.DynamicUser dynamicUser, 7 [1] class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo[] CS$0$0000) 8 IL_0000: nop 9 IL_0001: newobj instance void Loong.Dynamic.DynamicUser::.ctor() 10 IL_0006: stloc.0 11 IL_0007: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>> Loong.Dynamic.Program/'<Main>o__SiteContainer0'::'<>p__Site1' 12 IL_000c: brtrue.s IL_0051 13 IL_000e: ldc.i4 0x100 14 IL_0013: ldstr "WriteLine" 15 IL_0018: ldnull 16 IL_0019: ldtoken Loong.Dynamic.Program 17 IL_001e: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 18 IL_0023: ldc.i4.2 19 IL_0024: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo 20 IL_0029: stloc.1 21 IL_002a: ldloc.1 22 IL_002b: ldc.i4.0 23 IL_002c: ldc.i4.s 33 24 IL_002e: ldnull 25 IL_002f: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, 26 string) 27 IL_0034: stelem.ref 28 IL_0035: ldloc.1 29 IL_0036: ldc.i4.1 30 IL_0037: ldc.i4.0 31 IL_0038: ldnull 32 IL_0039: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, 33 string) 34 IL_003e: stelem.ref 35 IL_003f: ldloc.1 36 IL_0040: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, 37 string, 38 class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Type>, 39 class [mscorlib]System.Type, 40 class [mscorlib]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>) 41 IL_0045: call class [System.Core]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>>::Create(class [System.Core]System.Runtime.CompilerServices.CallSiteBinder) 42 IL_004a: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>> Loong.Dynamic.Program/'<Main>o__SiteContainer0'::'<>p__Site1' 43 IL_004f: br.s IL_0051 44 IL_0051: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>> Loong.Dynamic.Program/'<Main>o__SiteContainer0'::'<>p__Site1' 45 IL_0056: ldfld !0 class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>>::Target 46 IL_005b: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>> Loong.Dynamic.Program/'<Main>o__SiteContainer0'::'<>p__Site1' 47 IL_0060: ldtoken [mscorlib]System.Console 48 IL_0065: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 49 IL_006a: ldloc.0 50 IL_006b: ldfld object Loong.Dynamic.DynamicUser::Nickname 51 IL_0070: callvirt instance void class [mscorlib]System.Action`3<class [System.Core]System.Runtime.CompilerServices.CallSite,class [mscorlib]System.Type,object>::Invoke(!0, 52 !1, 53 !2) 54 IL_0075: nop 55 IL_0076: call string [mscorlib]System.Console::ReadLine() 56 IL_007b: pop 57 IL_007c: ret 58 } // end of method Program::Main
由此可见,编译器需要做的优化工作是很多的。复杂的操作也会带来效能上可见的损耗。
2.DLR ScriptRuntime
DLR(Dynamic Language Runtime),是微软的一个开源项目。为.NET应用程序提供了动态脚本注入技持。DLR构建的功能包含两个方面,一个是共享的动态类型系统,一个是标准的承载模型。但是VS2010并没有集成相关dll,大家可以从Codeplex获得源码,目前最新版本为1.0。这里不得不提到CLR,它是整个运行环境的基础,DLR也是运行在其之上的,这样是有显而易见的好处的,CLR的垃圾回收、JIT编译、安全模型,DLR也能享用这些底层架构功能,如果我们对垃圾回收进行优化,或者是提供某种功能,那么DLR相应的也能享用这种便利。DLR内部为了提高执行效率,会从自己先compile script,然后cache。有些类似JIT机制。避免重复加载,解析外部脚本。
在实际业务逻辑中,我们希望能够实时动态执行存储在文件中的代码或者完整一个业务逻辑功能,甚至我们可以动态选择脚本语言引擎,在应用程序域中动态生成脚本,并注入脚本,来达到控制业务逻辑的目的。在随笔一,我列举了一个场景,需要随意调整销售策略在所有POS机上,假设我们根据客户等级来进行折扣,VIP客户8折,荣誉客户9折,一般客户9.8折,并且金额达到5000时,再能进行9.8的折扣。UI我们假设使用winform。界面如下。
在程序中我们首先声明和初始化ScriptRuntime环境,设置ScriptEngine,创建ScriptSource和ScriptScope。然后通过外部Python脚本来执行我们折扣策略。
1 private void button1_Click(object sender, EventArgs e) 2 { 3 int level = 0; 4 if (this.rbtnVIP.Checked) 5 { 6 level = 0; 7 } 8 else if (this.rbtnCredit.Checked) 9 { 10 level = 1; 11 } 12 else 13 { 14 level = 2; 15 } 16 17 ScriptRuntime scriptRuntime = ScriptRuntime.CreateFromConfiguration(); 18 ScriptEngine pythEng = scriptRuntime.GetEngine("Python"); 19 20 //create the ScriptSource from the local file. 21 ScriptSource source = pythEng.CreateScriptSourceFromFile("Disc.py"); 22 23 //create a new ScriptScope, and set parameters for script. 24 ScriptScope scope = pythEng.CreateScope(); 25 scope.SetVariable("level", level); 26 scope.SetVariable("amt", Convert.ToDecimal(this.txtAmount.Text)); 27 28 //execute the script 29 source.Execute(scope); 30 31 this.textBox1.Text = scope.GetVariable("amtPay").ToString(); 32 this.textBox2.Text = scope.GetVariable("discAmt").ToString(); 33 }
Python脚本如下:
1 amtPay=amt 2 if level==0: 3 amtPay=amt*0.8 4 elif level==1: 5 amtPay=amt*0.9 6 else: 7 amtPay=amt*0.98 8 if amt>5000: 9 amtPay=amtPay*0.98 10 discAmt=amt-amtPay
在上述代码中,我们设置ScriptRuntime从configuration文件初始化,由此可以动态的选择创建Script Engine。configution文件配置如下
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="microsoft.scripting" type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" requirePermission="false" /> </configSections> <microsoft.scripting> <languages> <language names="IronPython;Python;py" extensions=".py" displayName="IronPython 2.6.1" type="IronPython.Runtime.PythonContext, IronPython, Version=2.6.10920.0, Culture=neutral, PublicKeyToken=null" /> </languages> </microsoft.scripting> </configuration>
在本例配置文件中,设置了IronPython语言引擎的几个属性。ScriptRuntime会根据Language节点来获取一个对ScriptEngine的引用。可以把ScriptEngine当成整个Runtime的大脑,正是由它来执行脚本的。脚本是如何给加载读取的呢,这就是ScriptSource要做的工作了,它访问并操作脚本文件的源代码,加载到Runtime之后,然后解析脚本,并编译脚本到CompiledCode中,生成表达式树加以缓存。如果多次执行相同的脚本,只需要一次加载编译即可。Runtime自己会根据实际需要优化脚本便于执行,从而提高了执行效率。这是实际上和dynamic对象执行机制是一样的。执行完操作后,需要把结果返回给当前程序,这时需要通过ScirptScope来完成。通过ScriptScope的方法SetVariable(string name, object value);来绑定输入变量到ScriptScope中,执行后,通过Execute(ScriptScope scope)执行操作,通过dynamic GetVariable(string name)获取结果。当然返回值是一个dynamic对象。
打个比方吧,可以把一个人体当成是ScriptRuntime,他是一个有机整体环境,ScriptEngine是人的大脑,用思考处理事务的地方。ScriptSource类似眼睛,耳朵的功能,用于收集信息,并初步解析。然后通过大脑判断,来控制手脚活动,手脚也就类似ScriptScope的功能。下面来做一个简单的执行图:
3.DynamicObject & ExpandoObject
DynamicObject & ExpandoObject能够使我们根据实际需要动态创建自己所需的对象。区别在于ExpandoObject是一个seal class,可以直接使用。DynamicObject则需要override几个方法。
构建一个对象,必须包含该对象的一些描述(属性,字段等)和该对象的行为(方法),但是在这样一个场合中,我们事先都不知道需要创建的对象有什么属性或支持方法。DynamicObject正是适合这种情形。下面我们尝试代码演示下,先定义动态类。


1 public class DynamicUser : DynamicObject 2 { 3 private Dictionary<string, object> _dyData = new Dictionary<string, object>(); 4 5 public override bool TryGetMember(GetMemberBinder binder, out object result) 6 { 7 if (_dyData.ContainsKey(binder.Name)) 8 { 9 result = _dyData[binder.Name]; 10 return true; 11 } 12 else 13 { 14 result = "property couldn't be found"; 15 return false; 16 } 17 } 18 19 public override bool TrySetMember(SetMemberBinder binder, object value) 20 { 21 _dyData[binder.Name] = value; 22 return true; 23 } 24 25 public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) 26 { 27 dynamic method = _dyData[binder.Name]; 28 result = method(args[0].ToString()); 29 return result != null; 30 } 31 }
我们把该对象的member信息都放在一个dictionary里。Get和Set的时候都去轮检这个dictionary。NET内部调用这些重载方法,在IL中,我们可以看到DynamicObject是如何处理这些绑定的。内部使用了System.Runtime.CompilerServices.CallSite和System.Runtime.CompilerServices.CallSiteBinder.CallSite,这和dynamic是一样的,详细请看 深入了解Dynamic & DLR
执行代码如下:添加一个GetMsg的方法,该方法接受一个参数为string,返回一个welcome string,我们使用lambda表达式来初始化该方法。


1 static void Main(string[] args) 2 { 3 dynamic user = new DynamicUser(); 4 user.NickName = "Loong"; 5 Console.WriteLine(user.GetType()); 6 Console.WriteLine(user.NickName); 7 8 Func<string, string> GetWelcomeMsg = name => string.Format("welcome {0}", name); 9 user.GetMsg = GetWelcomeMsg; 10 Console.WriteLine(user.GetMsg("Loong")); 11 12 Console.ReadLine(); 13 }
执行结果如下:
ExpandoObject的工作方式类似,不必重写方法。
1 dynamic expUser = new ExpandoObject(); 2 expUser.NickName = "loong2"; 3 Console.WriteLine(expUser.GetType()); 4 Console.WriteLine(expUser.NickName); 5 6 Func<string, string> GetWelcomeMsg = name => string.Format("welcome {0}", name); 7 expUser.GetMsg = GetWelcomeMsg; 8 Console.WriteLine(expUser.GetMsg("Loong"));
ExpandoObject使用上和dynamic没什么区别, 有一点区别是不能仅仅是创建dynamic对象,而不去初始化它。创建dynamic的同时必须初始化它。另外ExpandoObject的GetType方法总是显示System.Dynamic.ExpandoObject.而DynamicObject则显示继承类的type。一般来说dynamic和ExpandoObject就可以满足我们的需要, 如果需要准确控制对象的binding,可以尝试使用DynamicObject,重写GET、SET方法来满足自己的需要。
以上是个人对Dynamic和DLR的一些认识和学习。欢迎各位童鞋互相探讨。