1 動態語言簡介
支持動態特性的語言現在大行其道,並且有繼續增長的趨勢。比如 Ruby 和 Python, 還有天王級的巨星 --- JavaScript. 現在一個程序員說自己對 JavaScript 根本沒使用過,別人一定把你當成從火星回來的吧!
很多使用過 JavaScript 的程序員,剛開始對其動態特性深惡痛絕,欲除之而后快,但是一旦熟悉這個語言以后,又會發瘋般的愛上她(我的野蠻女友)。
以創建一個“人”為例, JavaScript 可以這樣寫:
var person = { FirstName:"Alan", LastName:"Yang" }; person.Age = 28; person.show = function(){ var that = this; return "My name is " + that.FirstName + " " + that.LastName + ". I’m " + that.Age + "years old " ; } person.show();
在Chrome 中運行的輸出為:
雖然,這是一段很簡單的代碼,但是已有幾個點我想大家都已經注意到:
- 屬性 Age 是在person 對象定義后動態添加的
- 方法 show是在person 對象定義后動態添加的
- Age 屬性和 show 方法正常使用
JavaScript 這種動態特性,使其在處理web 領域大顯身手,也使得JavaScript 成為端處理的當之無愧的王者,App 怎么寫 – JavaScript (WinJS), Web 頁面怎么寫—JavaScript (JQuery, KnocoutJS, ….), Server 怎么寫 --- JavaScript(Note.JS)。
One- JavaScript, Everywhere.
2 C# 怎么辦
What about C#? What about C# developer? What about me?
作為一名C# 開發人員,作為一名光榮的OOP 的擁躉,我們被一遍又一遍的告訴,“先聲明后使用”,強類型, 多態,繼承,引用….
不是我不明白,只是世界變化太快。昨天程序員還高大上,今天就矮窮挫。
難道這就是命么?難道C#/.NET 就沒有辦法處理動態增長了么?難道沒有如果了么?
等一等,C# 中貌似,好像,大概,不一定,是有動態特性的吧,就好像程序員的世界里還是有MM的:
- 君不見 var 到處飛?
- 君不見 dynamic 到處有?
- 君不見 Asp.Net MVC ViewBag 類經常被使用?
沒念及此,略感寬慰,var, dynamic, ViewBag 使用方便,還沒有負作用,是極好的?是在編碼,修Bug 必備良葯呀?
3 var V.S dynamic 關鍵字
3.1 var 關鍵字
沒有 var 的日子里,我需要知道每個變量的類型;沒有 var 的日子里;我需要提醒自己類型轉換;沒有var 的日子里,我每天問自己類型轉換安全么。沒有var 的日子里,寫代碼很痛苦。
var -- Beginning in Visual C# 3.0, variables that are declared at method scope can have an implicit type var. An implicitly typed local variable is strongly typed just as if you had declared the type yourself, but the compiler determines the type.
官方定義就是 NB, 但是說人話以后,就是 從C#3.0 引入var 關鍵字,只能用於方法體內, 當然包括匿名方法和Lambda表達式,編譯器會解析變量類型。
但是如何理解 var 關鍵字?
.NET 世界里有一個非常強大的武器,我已經很多關於C#/.NET 研究文章中使用過。那就是反射,不論你披了多么美味的語法糖果( syntax sugar), 我們都可以使用照妖鏡-反射使其顯出原形。
先進一段廣告—不,先近一段代碼—
static void Main(string[] args) { var vari = 1; int inti = 1; }
看一下反編譯后的IL 代碼:-- 使用工具為 ildasm
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 6 (0x6) .maxstack 1 .locals init ([0] int32 vari, [1] int32 inti) IL_0000: nop IL_0001: ldc.i4.1 IL_0002: stloc.0 IL_0003: ldc.i4.1 IL_0004: stloc.1 IL_0005: ret } // end of method Program::Main
見證奇跡的時刻. .locals init ([0] int32 vari,[1] int32 inti) – 大致意思是有有兩個局部變量,都是int類型,一個名字叫做vari, 一個名字叫做inti。
What? 明明我聲名了一個 var de 變量 vari, 初始化為 1; 為什么變成了 int 類型。發生了什么事?
Compiler 分析了var 聲名的值的類型,然后譯為強類型的聲名。這也就解釋了為什么 var 類型變量必須初始化,而且聲名后不能改變其類型,如下代碼不能編譯的。
static void Main(string[] args) { var vari = 1; vari = "test"; int inti = 1; }
哦!編譯器,編譯器,你欺騙了我?這個var 不同於 JavaScript 中的var 可以在生命后改變變量的類型。
有了var, 媽媽再也不用擔心我的方法中類型轉換的錯誤。以至於,現在C# 編碼規范要求程序員在方法體內所有變量的聲名均使用var 關鍵字。如ReSharper 工具。
在這里,我也強烈推薦在方法體內變量聲明均使用var 關鍵字。
3.2 dynamic 關鍵字
雖然我們有了var 關鍵字,但是還是不能和 JavaScript 程序員一起愉快的玩耍,因為他們可以在改變變量的類型。
啊!多么痛的領悟!
痛定思痛,我們來觀察一下,為什么 JavaScript 可以很開心的在運行時改變變量的類型?
難道說?難道是?JavaScript 是解釋型語言,邊解釋邊執行,沒有任何編譯文件,你沒有看到過jsdll, 或者jsjar 文件吧?C# 是編譯型語言。
那么問題就來了,如何讓C# 編譯器不檢查變量的類型那?相信這個問題很好回答,就好像皇帝如何給一個人特權,很明顯發個尚方寶劍,或者其他信物,然后見寶劍(信物),如朕親臨,不聽,先斬后奏。
於是乎?dynamic 關鍵字,應運而生 – 奉天承運,皇帝詔曰,dynamic 關鍵字可以不經過C# 編譯器檢查。接旨,吾皇萬歲,萬歲,萬萬歲—C#, Complier。
OK,看一下 dynamic 關鍵字的官方解釋---The dynamic type enables the operations in which it occurs to bypass compile-time type checking. Instead, these operations are resolved at run time。In most cases, it functions like it has type object. At compile time, an element that is typed as dynamic is assumed to support any operation。
以我小學英語畢業證的名義,我認為它說的是, dynamic 類型及其操作可以繞過 C# 編譯器的檢查,這些檢查將在運行時進行。在大部分情況下 dynamic 類型的功能類似於 object, object耶 – 只有想不到,沒有它做不到的事情喲!在編譯時,dynamic 元素被認為可以支持任何操作。(如有不嚴謹,請以大學英語4級的名義,拍磚)。
但是,為什么你總是要問問題?你是十萬個為什么么?好吧,你要問什么?
C# 不是靜態類型的語言么?這是怎么可能?為什么?
來吧,再近一段代碼—
static void Main(string[] args) { dynamic dyni = 1; dyni = "test"; }
你知道我要做什么了,反編譯—ildasm:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 15 (0xf) .maxstack 1 .locals init ([0] object dyni) IL_0000: nop IL_0001: ldc.i4.1 IL_0002: box [mscorlib]System.Int32 IL_0007: stloc.0 IL_0008: ldstr "test" IL_000d: stloc.0 IL_000e: ret } // end of method Program::Main
看一下.locals init ([0] object dyni) – 說明IL 認為你聲名了一個 object 類型的局部變量,然后 IL_0001: ldc.i4.1; IL_0002: box [mscorlib]System.Int32 對其進行初始化,並且進行裝箱操作(box) ,然后通過ldstr 把它的值初始化為 “test”。
你明白了么?你點點頭,然后又搖頭—你為什么要搖頭,我恨你?然后說—這個解釋了它最底層的運作原理,但是沒有解釋dynamic 到底是如何工作的?
好吧!當一切又回到原點,當一切努力都隨風而去,當生活沒有了方向,當…..
我們該怎么辦?大喊救命啊!--somebody help me! 但是沒有來幫助你,除了你自己?
嗯!我們有什么,我們要什么,然后我們怎么做? -- 解決問題的三個很好的問題。
- 第一個問題, 我們有什么?
我們有 dynamic 關鍵字聲名的變量,或者屬性。
- 第二個問題,我們要什么?
這個變量可以正確的識別它的所有值/方法,可以正確的調用。
- 第三個問題,我們怎么辦?
怎么辦?涼拌。
在程序的世界里,我們經常使用且有時被濫用的一個技巧是—引入第三者,引入一個層級,專門負責某種功能。想一想,Façade,Factory, Builder, Bridge … 等等設計模式,想一想 LINQ Provider, 想一想 CLR, Windows Runtime 等待。
那么同樣的,我們也應該有一個層級來支持 dynamic 關鍵字的運行,因為它是在運行時動態調用 dynamic 類型的方法和屬性。 -- 那我們就叫它 DLR 吧(Dynamic Language Runtime)
3.2.1DLR
果然不出山人所料?在CLR 上添加一個 DLR 層,來隔離 dynamic 代碼和實際生成的 IL 代碼。
來研究一段代碼(不研究代碼的程序員,就像不研究打仗的軍人一樣)--
class Program { static void Main(string[] args) { dynamic dynamicClass = new DynamicClass(); dynamicClass.Value = "Test Dynamic"; StaticClass staticClass = new StaticClass(); staticClass.Value = "Test Dynamic"; } } public class DynamicClass { public dynamic Value { get; set; } } public class StaticClass { public object Value { get; set; } }
為了對比,首先我們將有關 dynamic 的部分從 Main 方法中注釋掉,然后查看他的 IL 代碼—
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 2 .locals init ( [0] class ConsoleApplication1.StaticClass staticClass) L_0000: nop L_0001: newobj instance void ConsoleApplication1.StaticClass::.ctor() L_0006: stloc.0 L_0007: ldloc.0 L_0008: ldstr "Test Dynamic" L_000d: callvirt instance void ConsoleApplication1.StaticClass::set_Value(object) L_0012: nop L_0013: ret }
不解釋。
現在,我們把注釋去掉,然后將StaticClass 的部分注釋掉,看看發生了什么事情—
entrypoint .maxstack 7 .locals init ( [0] object dynamicClass, [1] class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo[] CS$0$0000) L_0000: nop L_0001: newobj instance void ConsoleApplication1.DynamicClass::.ctor() L_0006: stloc.0 L_0007: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1 L_000c: brtrue.s L_004b L_000e: ldc.i4.0 L_000f: ldstr "Value" L_0014: ldtoken ConsoleApplication1.Program L_0019: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) L_001e: ldc.i4.2 L_001f: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo L_0024: stloc.1 L_0025: ldloc.1 L_0026: ldc.i4.0 L_0027: ldc.i4.0 L_0028: ldnull L_0029: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string) L_002e: stelem.ref L_002f: ldloc.1 L_0030: ldc.i4.1 L_0031: ldc.i4.3 L_0032: ldnull L_0033: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string) L_0038: stelem.ref L_0039: ldloc.1 L_003a: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::SetMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [mscorlib]System.Type, class [mscorlib]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>) L_003f: call class [System.Core]System.Runtime.CompilerServices.CallSite`1<!0> [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>>::Create(class [System.Core]System.Runtime.CompilerServices.CallSiteBinder) L_0044: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1 L_0049: br.s L_004b L_004b: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1 L_0050: ldfld !0 [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>>::Target L_0055: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1 L_005a: ldloc.0 L_005b: ldstr "Test Dynamic" L_0060: callvirt instance !3 [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>::Invoke(!0, !1, !2) L_0065: pop L_0066: ret
在看一下 dynamic 屬性的 IL 代碼 – 屬性類型為object喲!
.property instance object Value { .get instance object ConsoleApplication1.DynamicClass::get_Value() .set instance void ConsoleApplication1.DynamicClass::set_Value(object) .custom instance void [System.Core]System.Runtime.CompilerServices.DynamicAttribute::.ctor() }
My Lady Gaga!
我做了什么,相比靜態類型,生成了這么多的代碼。人哪,還是不要知道那么多呀,知道多了影響壽命。
讓我大致的解釋一下上面的IL 代碼,你讓我復雜的說,我也不會。
- 第一: RuntimeBinder – 做什么用的?我不知道,但是Binder 這個名字說明了,有源,有目標對象,不源對象綁定到目標對象上。
說着,說着,我就想到了反射,當用反射給一個屬相賦值時,需要找到屬性的Set 方法,把值作為參數,然后再目標對象上Invoke。
很接近哦,這里創建的CSharpArugumentInfo[] – 參數列表。
[1] class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo[] CS$0$0000)
- 第二:CallSite – Call,Invoke? 難道這個是用來Invoke 方法的。
_0007: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1
- 第三:注意 ldstr “Value” – Value 是什么,Value 是我們的屬性名。為什么把屬性當作字符加載進來。而不是類型與靜態類型的把Value 當作方法來調用?( L_000d: callvirt instance void ConsoleApplication1.StaticClass::set_Value(object))。
- 第四:果然把 Value 當作參數加載, 注意idc.i4.0
L_001f: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
L_0024: stloc.1
L_0025: ldloc.1
L_0026: ldc.i4.0
- 第五:方法調用,
1) 加載CallSite 類的 Target 字段—
L_0050: ldfld !0 [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>>::Target
2) 加載CallSite類p_Stie1, 自動生成的:
System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> 這個委托,第一個參數CallSite, 第二個參數Target,
第三個參數屬性名,第四個參數需要賦的值.
L_0055: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>> ConsoleApplication1.Program/<Main>o__SiteContainer0::<>p__Site1
3) L_005a: ldloc.0
4) 加載參數值—
L_005b: ldstr "Test Dynamic"
5) 在目標對象上調用方法—
L_0060: callvirt instance !3 [mscorlib]System.Func`4<class [System.Core]System.Runtime.CompilerServices.CallSite, object, string, object>::Invoke(!0, !1, !2)
哦!實際上是用反射+Object 來處理運行時的動態特性!鑒定完畢。
3.2.2DLR 重要概念
- Expression Tree—DLR 使用 ExpressionTree 來表示語法。所以,DLR擴展了LINQ的 ExpressionTree 用來控制流程,賦值等。
- CallSite—負責運行時的源,目標對象的查找
- CallSiteBinder – 負責綁定
- Call site Cache – 通過上面的分析我們可以得出一個結論,那就是 dynamic 使用了反射,而且進行很多檢查,這回降低代碼運行效率。CallSite 會把分析經過 CallSite 調用的動態方法緩存,從而提高使用效率。
- 如果在運行時,沒有找到需要調用的目標方法,則拋出RuntimeBinderException 異常。
3.2.3dynamic 關鍵字的用法
我們可以把 dynamic 關鍵字認為成object 關鍵字。也就是說所以可以使用 object 關鍵字的地方,基本上都可以使用 dynamic 關鍵字。 -- 以實際情況為准,沒有全部進行測試。
- 聲名變量
dynamic dyn = 1; Console.WriteLine(dyn); dyn = "test"; Console.WriteLine(dyn.GetType());
- 聲名方法返回值
static dynamic ReturnDynamic(int type) { if(type == 0) { return 1; } else { return "you passed a type other than " + 0; } }
- 聲名屬性
public dynamic Value { get; set; }
- 委托的泛型參數
Func<dynamic, int> func = (i) => int.Parse(i.ToString()); Console.WriteLine(func("1")); Console.WriteLine(func(1));
4 DynamicObject V.S. ExpandoObject
現在我們有了var, 有了dynamic, 但是還是不能和 JavaScript 程序員一起愉快的玩耍,因為,我們有沒有對象,沒有可以動態增長的對象。
好吧,人生就是這樣,永遠不要滿足? 有愛情,還要面包,有房子,還要到老,有車子,還要有鈔票。
於是DynamicObject 和ExpandoObject 被千呼萬喚后,橫空出世。
DynamicObejct 是所有動態類的基類,可以從其繼承以實現自己的動態對象。ExpandoObject 是一個sealed 類,已經封裝好了所有方法。
這個也給我們寫框架代碼給了一點提醒,定義好頂層的通常是抽象的基類后,然后實現一個子類
4.1 DynamicObject
static void Main(string[] args) { dynamic dynamicObject = new MyDynamicObject(); dynamicObject.FirstName = "Alan"; dynamicObject.LastName = "Yang"; dynamicObject.Age = 28; Action<dynamic> show = (item) => Console.WriteLine("My name is " + item.FirstName + " " + item.LastName + ". I'm " + item.Age + " years old."); dynamicObject.Show = show; dynamicObject.Show(dynamicObject); Console.Read(); } public class MyDynamicObject : DynamicObject { Dictionary<string, object> _dynamicData = new Dictionary<string, object>(); public override bool TryGetMember(GetMemberBinder binder, out object result) { bool success = false; result = null; if (_dynamicData.ContainsKey(binder.Name)) { result = _dynamicData[binder.Name]; success = true; } else { result = "Property Not Found!"; success = false; } return success; } public override bool TrySetMember(SetMemberBinder binder, object value) { _dynamicData[binder.Name] = value; return true; } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = true; dynamic method = _dynamicData[binder.Name]; method(args[0]); return true; } }
4.2 ExpandoObject
static void Main(string[] args) { dynamic dynamicObject = new ExpandoObject(); dynamicObject.FirstName = "Alan"; dynamicObject.LastName = "Yang"; dynamicObject.Age = 28; Action<dynamic> show = (item) => Console.WriteLine("My name is " + item.FirstName + " " + item.LastName + ". I'm " + item.Age + " years old."); dynamicObject.Show = show; dynamicObject.Show(dynamicObject); Console.Read(); }
4.3 ViewBag 源代碼
最后貼出來 ASP.Net MVC 中動態對象 ViewBag 的代碼,讓 ViewBag不再神秘。ViewResult 中的ViewBag 屬性的定義為:
public dynamic ViewBag { get { if (_dynamicViewData == null) { _dynamicViewData = new DynamicViewDataDictionary(() => ViewData); } return _dynamicViewData; } }
DynamicViewDataDictionary 類的定義為:
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; namespace System.Web.Mvc { internal sealed class DynamicViewDataDictionary : DynamicObject { private readonly Func<ViewDataDictionary> _viewDataThunk; public DynamicViewDataDictionary(Func<ViewDataDictionary> viewDataThunk) { _viewDataThunk = viewDataThunk; } private ViewDataDictionary ViewData { get { ViewDataDictionary viewData = _viewDataThunk(); Debug.Assert(viewData != null); return viewData; } } // Implementing this function improves the debugging experience as it provides the debugger with the list of all // the properties currently defined on the object public override IEnumerable<string> GetDynamicMemberNames() { return ViewData.Keys; } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = ViewData[binder.Name]; // since ViewDataDictionary always returns a result even if the key does not exist, always return true return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { ViewData[binder.Name] = value; // you can always set a key in the dictionary so return true return true; } } }
5 結論
C# 中為了支持動態語言特性,引入了 var, dynamic, anonymous object (匿名對象) 等。但是作為強類型語言,動態特性是通過引入更多的復雜度來實現的,這與腳本語言,動態增長語言有着本質的區別。