1. 程式人生 > 程式設計 >一文帶你瞭解 C# DLR 的世界(DLR 探祕)

一文帶你瞭解 C# DLR 的世界(DLR 探祕)

在很久之前,我寫了一片文章詳解C# 匿名物件(匿名型別)、var、動態型別 dynamic,可以借鑑。因為那時候是心中想當然的認為只有反射能夠在執行時解析物件的成員資訊並呼叫成員方法。後來也是因為其他的事一直都沒有回過頭來把這一節知識給補上,正所謂亡羊補牢,讓我們現在來大致瞭解一下DLR吧。

DLR 全稱是 Dynamic Language Runtime(動態語言執行時)。這很容易讓我們想到同在C#中還有一個叫 CLR 的東西,它叫 Common Language Runtime。那這兩者有什麼關係呢?這個後續再說

C#4動態功能是Dynamic Language Runtime(動態語言執行時,DLR)的一部分.DLR是新增到CLR的一系列服務,它允許新增動態語言,如Ruby和Python,並使C#具備和這些動態語言相同的某些功能.

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 這片文章的一副結構圖的意思

一文帶你瞭解 C# DLR 的世界(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>) staticCallSite1.Target)((CallSite) staticCallSite1,obj1,6);
 staticCallSite2 = staticCallSite2 ?? CallSite<Action<CallSite,Type,object>>.Create(Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded,"WriteLine",(IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2]
 {
 CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType,CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,(string) null)
 }));

 ((Action<CallSite,object>) staticCallSite2.Target)((CallSite) staticCallSite2,typeof (Console),obj2);

上文的兩個變數staticCallSite1,staticCallSite2 是靜態變數,起到快取的作用。

這裡涉及到了DLR核心三個概念

  1. ExpressTree(表示式樹):通過CLR執行時用抽象語法樹(AST)生成程式碼並執行。並且它也是用來與動態語言互動的主要工具(如Python,JavaScript 等)
  2. CallSite(呼叫點):當我們寫的呼叫動態型別的方法,這就是一個呼叫點。這些呼叫都是靜態函式,是能夠快取下來的,所以在後續的呼叫,如果發現是相同型別的呼叫,就會更快的執行。
  3. Binder(繫結器):除了呼叫點之外,系統還需要知道這些方法如何呼叫,就比如例子中的通過呼叫Binder.InvokeMember方法,以及是那些物件型別呼叫的方法等資訊。繫結器也是可以快取的

總結

DLR執行過程我們總結起來就是,在執行時DLR利用編譯執行期間生成的表示式樹、呼叫點、繫結器程式碼,以及快取機制,我們就可以做到計算的重用來達到高效能。ASP.NET頁面快取常見的4種方式

現在我們就知道了為什麼DLR能幹出與反射相同的效果,但是效能要遠比反射要高的原因了。

補充說明

剛看到評論裡的同學提到了reflection與dynamic的效能測試比較,發現反射效能佔據明顯的優勢。事實上,從那個例子來看,恰恰說明了DLR的問題。這裡我先列出他的測試程式碼

const int Num = 1000 * 100;
{
 var mi = typeof(XXX).GetMethod("Go");
 var go1 = new XXX();
 for (int i = 0; i < Num; i++)
 {
 mi.Invoke(go1,null);
 }
}
{
 dynamic go1 = new XXX();
 for (int i = 0; i < Num; i++)
 {
 go1.Go();
 }
}

在這個測試中,已經將反射出來的元資料資訊快取到區域性變數 mi,所以在呼叫方法的時候,實際上用到的是已經快取下來的 mi。那麼在沒有快取優勢的情況,說明DLR效能是不如 MethodInfo+Invoke 的。

其實在文章總結的時候也強調了,利用快取機制達到多次重複計算的重用來提高效能

那麼我們在看一個例子:

public void DynamicMethod(Foo f) {
 dynamic d = f;
 d.DoSomething();
}

public void ReflectionMethod(Foo f) {
 var m = typeof(Foo).GetMethod("DoSomething");
 m?.Invoke(f,null);
}

方法 DoSomething 只是一個空方法。現在我們來看執行結果

// 執行時間
var f = new Foo();
Stopwatch sw = new Stopwatch();
int n = 10000000;
sw.Start();
for (int i = 0; i < n; i++) {
 ReflectionMethod(f);
}
sw.Stop();
Console.WriteLine("ReflectionMethod: " + sw.ElapsedMilliseconds + " ms");

sw.Restart();
for (int i = 0; i < n; i++) {
 DynamicMethod(f);
}
sw.Stop();
Console.WriteLine("DynamicMethod: " + sw.ElapsedMilliseconds + " ms");

// 輸出
ReflectionMethod: 1923 ms
DynamicMethod: 223 ms

這裡我們就能明顯看出執行時間的差距了。實際上DLR的執行過程我用下面偽程式碼表示

public void DynamicMethod(Foo f) {
 dynamic d = f;
 d.DoSomething();
}
// 以下是DLR會生成大概的程式碼
static DynamicCallSite fooCallSite;
public void ReflectionMethod(Foo f) {
 object d = f;
 if(fooCallSite == null) fooCallSite = new DynamicCallSite();
 fooCallSite.Invoke("Foo",d);
}

編譯器在編譯上述方法DynamicMethod時,會詢問一次這個呼叫點呼叫的方法的型別是否是同一個,如果是則直接將已經準備好的呼叫點 fooCallSite 進行呼叫,否則則像文章之前說的,會生成呼叫點,繫結器繫結成員資訊,根據AST將表示式生成表示式樹,將這些都快取下來。在進行計算(呼叫)。

正因為我們知道了DLR的一些內幕,所以我們自然也知道了注意該如何用 DLR,以及關鍵字 dynamic。比如我們現在知道了C#編譯器會將 dynamic 等同 object 對待。那麼我們在使用的時候一定要注意不要被“莫名其妙”的被裝箱了,導致不必要的效能損失了。

至於 DLR 的應用,特別是結合動態語言進行程式設計,來達到靜態語言動態程式設計的目的。其實DLR剛出來之際,就有了如 IronPython 這樣的開源元件。這是另外一個話題,並且我們在做實際應用的情況也很少,所以就沒有展開來講了。

補充:

DLR主要提供以下三個功能:

1.語言實現服務提供語言的互操作性

2.動態語言執行時服務提供動態呼叫支援

3.公共指令碼宿主

依託這些模組,您可以非常輕鬆的做下面這些事

1.為您現有的.NET應用程式,加入指令碼支援

2.為您現有的語言,提供動態知己

3.為任何物件提供動態操作支援

4.在您的架構中提供指令碼語言.

引數資料:

https://www.codeproject.com/Articles/42997/NET-4-0-FAQ-Part-1-The-DLR《深入理解C#》

到此這篇關於一文帶你瞭解 C# DLR 的世界的文章就介紹到這了,更多相關C# DLR 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!