《深入理解C#》整理9-靜態語言中的動態綁定

一、何謂、何時、爲何、如何

1、何謂動態類型

C#是一門靜態類型語言。編譯器知道代碼中表達式的類型,知道任何類型中可用的成員。它應用了相當複雜的規則來決定哪個成員應該在何時使用。這包括了重載決策;在(動態類型出現)之前的唯一途徑是根據對象在執行時的類型,來選擇虛方法的實現。決定使用哪個成員的過程稱爲綁定(binding),對於靜態類型的語言來說,綁定發生在編譯時。而在動態類型的語言中,所有的綁定都發生在執行時。編譯器或解析器可以檢查語法是否正確,但卻無法檢查所調用的方法或所訪問的屬性是否真的存在。

值得一提的是,C# 4全新的動態特性不包含在執行時解釋C#源代碼的功能。當然,有時候,無論採取什麼方式,都能完成同樣的工作。由於讓編譯器在執行前進行了更多的準備工作,因此靜態系統的性能往往比動態系統更優。

2、動態類型什麼時候有用,爲什麼

動態類型立足於兩個有利要點:

①通常我們需要知道調用的成員名稱、要傳入的參數以及要調用的對象,但C#編譯器通常需要得更多。至關重要的是,爲了準確地確定成員(模型重載),我們需要知道所調用的對象的類型和參數的類型。我們有時無法在編譯時知道這些類型,即使你確實能保證代碼運行時成員會存在並且正確。

②動態類型的第二個重要的特性是,對象可以通過分析提供給它的名稱和參數來響應某個調用。其行爲就像是該類型正常地聲明瞭成員一樣,即使直到執行時我們才能知道成員的名稱。

使用動態語言進行編程還有一個特性,它往往是使用適當解釋器進行編程的實驗性風格。這一點並非與C# 4直接相關,但C# 4可以與運行在DLR(Dynamic Language Runtime,動態語言運行時)上的動態語言進行豐富的互操作,這意味着如果你要處理的問題可以從這種風格中受益,你就可以直接使用C#返回的結果,而不用後來再將其移植到C#中。

3、C# 4如何提供動態類型

C# 4引入了一個新的類型,稱爲dynamic。編譯器對待該類型的方式與普通的CLR類型不同。任何使用了動態值的表達式都會從根本上改變編譯器的行爲。編譯器不會試圖弄懂代碼的確切含義,不會恰當地綁定各個成員的訪問,不會執行重載決策。它只是通過解析源代碼,找出要執行的操作的種類、名稱、所涉及的參數以及其他相關信息。編譯器也不會發出(emit)IL來直接執行代碼,而是使用所有必要的信息生成調用DLR的代碼。剩下的工作將在執行時進行。

當DLR在執行時綁定相關調用時,確定應該發生什麼事情的過程非常複雜。在此期間,不僅僅要考慮方法重載等常規的C#規則,而且該對象本身也需要動態確定。

二、關於動態的快速指南

dynamic的主要規則如下:

  • 幾乎所有CLR類型都可以隱式轉換爲dynamic
  • 所有dynamic類型的表達式都可以隱式轉換爲CLR類型
  • 使用dynamic類型值的表達式通常會動態地求值
  • 動態求值表達式的靜態類型通常被視爲dynamic

三、幕後原理

1、DLR簡介

DLR即爲動態語言運行時(Dynamic Language Runtime),它是所有動態語言和C#編譯器用來動態執行代碼的庫。它真的僅僅只是一個庫。儘管它同樣以運行時爲名,卻與CLR(Common Language Runtime,公共語言運行時)不在同一個級別——它不涉及JIT編譯、本地API封送(marshal)、垃圾回收等內容。而是建立在大量.NET 2.0和.NET 3.5的功能之上,特別是DynamicMethod和Expression類型。

儘管DLR不直接操作本地代碼,但在某種程度上我們可以認爲它做着與CLR類似的工作:正如CLR將IL(中間語言)轉換爲本地代碼一樣,DLR將用綁定器、調用點(call site)、元對象(metaobject),以及其他各種概念表示的代碼轉換爲表達式樹,後者可以被編譯爲IL,並最終由CLR編譯爲本地代碼。如下圖,DLR一個很重要的部分是多級緩存(multilevel cache)

image-20201031143749735

2、DLR核心概念

DLR的目的可以非常籠統地概括爲,基於執行時才能知道的信息以高級形式表示並執行代碼。

2.1、調用點

即調用方法的地方,它有點像是DLR的原子——可以被視爲單個執行單元的最小代碼塊。一個表達式可能包含多個調用點,但其行爲是建立在固有方式之上的,即一次對一個調用點求值。調用點在代碼中表示爲一個System.Runtime.CompilerServices.CallSite

2.2、接收器和綁定器

除了調用點之外,我們還需要其他信息來判斷代碼的含義以及如何執行。在DLR中,有兩個實體可以用來進行判斷:接收器(receiver)和綁定器(binder)。調用的接收器是被調用的成員所在的對象。綁定器取決於調用語言,並且是調用點的一部分。C#特定的綁定器爲Microsoft.CSharp.RuntimeBinder. Binder

DLR總是將更高的優先級賦予接收器:如果動態類型知道如何處理調用,則將會使用該對象提供的執行路徑。一個對象如果實現了新的IDynamicMetaObjectProvider接口,就具備了動態特性,他只包含一個成員:GetMetaObject,要正確實現GetMetaObject,需要藉助於表達式樹

2.3、規則和緩存

如何執行一個調用所作出的決策,稱爲規則(rule)。從根本上來說,它包含兩個邏輯元素:調用點表現爲這種行爲時所處的環境以及行爲本身。規則的第二部分是當規則匹配時所使用的代碼,它表示爲一個表達式樹。它也可以存儲爲一個編譯好的供調用的委託,但使用表達式樹意味着可以對緩存進行深度優化。DLR中的緩存包含3個級別:L0、L1、L2。緩存以不同方式將信息存儲於不同的作用域中。每個調用點包含自己的L0和L1緩存,而L2緩存可能被多個類似的調用點共享:

image-20201031152652447

共享L2緩存的調用點是由它們的綁定器決定的——每個綁定器都包含一個與之相關的L2緩存。編譯器(或其他創建了調用點的東西)決定要使用多少個綁定器。它可以只對多個表示類似代碼的調用點使用一個綁定器,如果執行時的上下文相同,這些調用點應該以相同的方式執行。事實上,C#編譯器沒有使用這個功能——它會爲每個調用點都創建一個新的綁定器,因此對於C#開發者來說,L1和L2沒有太大區別。但真正的動態語言,如IronRuby和IronPython都進一步使用了該功能

C#編譯器生成代碼來簡單地執行調用點的L0緩存(通過Target屬性訪問的委託)。L0緩存包含單一的規則,將在調用時進行檢查。如果規則匹配,將執行相關的行爲。如果不匹配(或如果爲第一次調用,還沒有任何規則),將調用L1緩存,繼而調用L2緩存。如果L2緩存找不到任何匹配的規則,將要求接收器或綁定器來解決這個調用。其結果將被放入緩存供以後使用。

L1和L2緩存以相當標準的方式審覈它們的規則——每級緩存都包含一組規則,每條規則都會檢查是否匹配。L0緩存則略有不同。行爲的兩個部分(檢查規則和委派給L1緩存)將被合併爲單獨的方法,然後進行JIT編譯。對L0緩存進行更新時將根據新的規則重新構建方法

3、C#編譯器如何處理動態

在面對動態代碼時,C#編譯器的主要工作是解決什麼時候需要動態行爲,以及獲取所有必需的上下文,這樣綁定器和接收器在執行時就有足夠的信息來處理調用

①如果使用了動態,那麼它就是動態的。即如果調用的任何一部分爲動態的,該調用即爲動態的,並將以動態值的執行時類型來匹配重載;

②你不能將所有CLR類型都轉換爲object,CLR類型與dynamic之間對於轉換的限制與此類似;

③動態表達式並不總是動態地求值,在某些情況下,CLR完全可以使用普通的靜態求值路徑對錶達式進行求值,即使個別子表達式爲動態的;

④動態求值的表達式並不總是動態類型

⑤創建調用點和綁定器

4、更加智能的C#編譯器

C# 4能夠讓你跨越靜態和動態的邊界,不只是因爲一些代碼可以靜態綁定,一些可以動態綁定,它還能夠在一次綁定的過程中,將這兩種概念相結合。它能記住調用點中任何需要知道的信息,然後在執行時與動態值的類型進行合併

①在執行時保存編譯器行爲:計算出綁定器行爲的理想模式是,假設源代碼中沒有動態值,我們知道值的確切類型:即執行時實際值的類型。這僅適用於表達式中的動態值;任何在編譯時知道的類型,都仍將用於查找,如成員決策;

②動態代碼的編譯時錯誤:動態類型的缺點之一,就是將一些通常在編譯時可以檢測到的錯誤,推遲到了執行時,進而拋出異常;

5、動態代碼的約束

①不能動態處理擴展方法,這是由於動態代碼不知道調用所在的源文件中using指令到底引入了哪些命名空間。也就是說在執行時它不知道哪些擴展方法是可用的。這不僅意味着不能調用動態值的擴展方法,還意味着不能將動態值作爲參數傳入擴展方法。編譯器推薦了兩種變通方案。如果你知道使用哪個重載,可以在方法內將動態值轉換爲正確的類型。或者,假設你知道擴展方法所在的靜態類型,可以像調用普通的靜態方法那樣進行調用。

image-20201031155124230

②委託與動態類型之間轉換的限制:在轉換Lambda表達式、匿名方法或方法組時,編譯器需要知道委託(或表達式)的確切類型。你不能不加轉換就將它們設置爲簡單的Delegate或object變量。對於dynamic來說也是如此。強制轉換可以滿足編譯器的要求。如果稍後要動態地執行該委託,那麼這樣做在某些情況下就非常有用。你還可以將動態類型作爲委託的參數。

有必要指出的是,LINQ與dynamic的交互不會導致任何損失。你可以擁有一個以dynamic爲元素類型的強類型集合,你還可以使用擴展方法、Lambda表達式甚至查詢表達式。該集合可以包含不同類型的對象,它們在執行時將表現出恰當的行爲。

③構造函數和靜態方法:你可以通過指定動態實參的方式來動態調用構造函數和靜態方法,但你不能對一個動態類型調用構造函數或靜態方法。因爲你無法指定具體的類型。如果你遇到要使用這種動態的情況,可以考慮使用實例方法,例如創建工廠類型。你會發現可以使用簡單的多態和接口獲得動態行爲,而不必使用靜態類型。

④類型聲明和泛型類型參數:你不能聲明一個基類爲dynamic的類型。你同樣不能將dynamic用於類型參數的約束,或作爲類型所實現的接口的一部分。你可以將其用於基類的類型實參,或在聲明變量時將其用於接口

四、實現動態行爲

C#語言沒有爲實現動態行爲提供任何幫助,而.NET框架則不然。能夠動態響應的類型必須實現IDynamicMetaObjectProvider,大多數情況下內嵌的兩個實現都能完成大部分工作。我們將研究這兩個類型,並介紹一個非常簡單的IDynamicMetaObjectProvider實現,這三種方法互不相同

1、使用ExpandoObject

System.Dynamic.ExpandoObject只有一個無參的公共構造函數。除了各個接口的顯式實現外,它沒有公共方法。比較重要的接口爲IDynamicMetaObject Provider和IDictionary<string,object>。(它實現的其他接口均爲IDictionary<string, object>所擴展的接口。)它還是封閉的,所以不能繼承它從而實現有用的行爲。只有用dynamic引用或實現某個接口時,才能使用ExpandoObject。相關示例如下:

image-20201031160900367

2、使用DynamicObject

DynamicObject與DLR的交互比ExpandoObject要更加強大,且比實現IDynamic MetaObjectProvider要簡單得多。儘管它並不是一個真正的抽象類,但只有繼承它才能做些有用的事情——而且唯一的構造函數還是受保護的,因此將其視爲抽象類可能更加實際。你可能需要覆蓋4類方法:

  • TryXXX()調用方法,表示對對象的動態調用;
  • GetDynamicMemberNames(),返回可用成員的列表;
  • 普通的Equals()、GetHashCode()和ToString()方法仍然可以照常覆蓋;
  • GetMetaObject(),返回DLR使用的元對象。

3、實現IDynamicMetaObjectProvider

實現IDynamic MetaObjectProvider的難點並不是接口本身,而是構建該接口的唯一方法所返回的Dynamic MetaObject。DynamicMetaObject有點像DynamicObject,它包含很多方法,我們可以覆蓋它們,並影響相應的行爲。但在被覆蓋的方法內,它不會直接處理所需的行爲,而會構建一個表達式樹來描述行爲以及行爲產生的環境。這種額外的間接層就是它稱爲元對象的原因。示例:

無標題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章