一文帶你瞭解 C DLR 的世界 在很久之前,我寫了一片文章 "dynamic結合匿名類型 匿名對象傳參" ,裡面我以為DLR內部是用反射實現的。因為那時候是心中想當然的認為只有反射能夠在運行時解析對象的成員信息並調用成員方法。後來也是因為其他的事一直都沒有回過頭來把這一節知識給補上,正所謂亡羊補牢 ...
一文帶你瞭解 C# DLR 的世界
在很久之前,我寫了一片文章dynamic結合匿名類型 匿名對象傳參,裡面我以為DLR內部是用反射實現的。因為那時候是心中想當然的認為只有反射能夠在運行時解析對象的成員信息並調用成員方法。後來也是因為其他的事一直都沒有回過頭來把這一節知識給補上,正所謂亡羊補牢,讓我們現在來大致瞭解一下DLR吧。
DLR 全稱是 Dynamic Language Runtime(動態語言運行時)。這很容易讓我們想到同在C#中還有一個叫 CLR 的東西,它叫 Common Language Runtime。那這兩者有什麼關係呢?這個後續再說
DLR 是 C#4.0 新引進來的概念,其主要目的就是為了動態綁定與交互。
C#關鍵字 dynamic
DLR 首先定義了一個核心類型概念,即動態類型。即在運行時確定的類型,動態類型的成員信息、方法等都只在運行時進行綁定。與CLR的靜態類型相反,靜態類型都是在C#編譯期間通過一系列的規則匹配到最後的綁定。
將這種動態進行綁定的過程它有點類似反射,但其內部卻和反射有很大的不同。這個稍微會談到。
由動態類型構成的對象叫動態對象。
DLR一般有下列特點:
- 把CLR的所有類型全部隱式轉成
dynamic
。如dynamic x = GetReturnAnyCLRType()
- 同樣,dynamic幾乎也可以轉換成CLR類型。
- 所有含有動態類型的表達式都是在運行期進行動態計算的。
DLR發展到現在,我們幾乎都使用了動態類型關鍵字 dynamic
以及還有引用DLR的類庫 Dapper等。
在我們不想創建新的靜態類做DTO映射時,我們第一時間會想到動態類型。也經常性的將dynamic作為參數使用。
這時候我們就要註意一些 dynamic 不為大多人知的一些細節了。
不是只要含有 dynamic 的表達式都是動態的。
什麼意思呢,且看這段代碼dynamic x = "marson shine";
。這句代碼很簡單,就是將字元串賦值給動態類型 x。
大家不要以為這就是動態類型了哦,其實不是,如果單單隻是這一句的話,C#編譯器在編譯期間是會把變數 x 轉變成靜態類型 object 的,等價於object x = "marson shine";
。可能有些人會驚訝,為什麼C#編譯器最後會生成object類型的代碼。這就是接下來我們要註意的。
dynamic 於 object 的不可告人的關係
其實如果你是以 dynamic 類型為參數,那麼實際上它就是等於 object 類型的。換句話說,dynamic在CLR級別就是object。其實這點不用記,我們從編譯器生成的C#代碼就知道了。
這裡我用的是dotpeek查看編譯器生成的c#代碼。
這裡順便想問下各位,有沒有mac下c#反編譯的工具。求推薦
所以我們在寫重載方法時,是不能以 object 和 dynamic 來區分的。
void DynamicMethod(object o);
void DynamicMethod(dynamic d); // error 編譯器無法通過編譯:已經存在同名同形參的方法
如果說 dynamic 與 object 一樣,那麼它與 DLR 又有什麼關係呢?
其實微軟提供這麼一個關鍵字,我認為是方便提供創建動態類型的快捷方式。而真正於動態類型密切相關的是命名空間System.Dynamic
下的類型。主要核心類DynamicObject,ExpandoObject,IDynamicMetaObjectProvider
,關於這三個類我們這節先不談。
DLR探秘
首先我們來大致瞭解C#4.0加入的重要功能 DLR,在編譯器中處於什麼層次結構。
在這裡我引用 https://www.codeproject.com/Articles/42997/NET-4-0-FAQ-Part-1-The-DLR 這片文章的一副結構圖的意思
動態編程 = CLR + DLR
這足以說明 DLR 在C#中的位置,雖然名字與CLR只有一個字母之差,但是它所處的層次其實是在CLR之上的。我們知道編譯器將我們寫的代碼轉換成IL,然後經由CLR轉換成本地代碼交由CPU執行可執行程式。那麼實際上,DLR 是在編譯期間和運行期做了大量工作。最後還是會將C#代碼轉換成CLR靜態語言,然後再經由 CLR 將代碼轉換成本地代碼執行(如調用函數等)。
現在我們來簡要介紹一下DLR在編譯期間做了什麼。
到這裡就不得不以例子來做說明瞭,我們就上面的例子稍加改造一下:
// program.cs
dynamic x = "marson shine";
string v = x.Substring(6);
Console.WriteLine(v);
為了節省篇幅,我簡化並改寫了難看的變數命名以及不必要的註釋。生成的代碼如下:
object obj1 = (object) "marson shine";
staticCallSite1 = staticCallSite1 ?? CallSite<Func<CallSite, object, int, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "Substring", (IEnumerable<Type>) null, typeof (Example), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, (string) null)
}));
object obj2 = ((Func<CallSite, object, int, object>) staticCallSite1.Target)((CallSite) staticCallSite1, obj1, 6);
staticCallSite2 = staticCallSite2 ?? CallSite<Action<CallSite, Type, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "WriteLine", (IEnumerable<Type>) null, typeof (Example), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null)
}));
((Action<CallSite, Type, object>) staticCallSite2.Target)((CallSite) staticCallSite2, typeof (Console), obj2);
上文的兩個變數staticCallSite1,staticCallSite2
是靜態變數,起到緩存的作用。
這裡涉及到了DLR核心三個概念
- ExpressTree(表達式樹):通過CLR運行時用抽象語法樹(AST)生成代碼並執行。並且它也是用來與動態語言交互的主要工具(如Python,JavaScript 等)
- CallSite(調用點):當我們寫的調用動態類型的方法,這就是一個調用點。這些調用都是靜態函數,是能夠緩存下來的,所以在後續的調用,如果發現是相同類型的調用,就會更快的運行。
- Binder(綁定器):除了調用點之外,系統還需要知道這些方法如何調用,就比如例子中的通過調用
Binder.InvokeMember
方法,以及是那些對象類型調用的方法等信息。綁定器也是可以緩存的
總結
DLR運行過程我們總結起來就是,在運行時DLR利用編譯運行期間生成的表達式樹、調用點、綁定器代碼,以及緩存機制,我們就可以做到計算的重用來達到高性能。在很早前從老趙的表達式樹緩存系列文章也指出了,利用表達式樹緩存性能最接近直接調用(當然不包括IL編程)。
現在我們就知道了為什麼DLR能幹出與反射相同的效果,但是性能要遠比反射要高的原因了。
參數資料: