本文章是由機器翻譯。

動態 .NET

了解 C# 4 的動態關鍵字

Alexandra Rusina

dynamic 關鍵字和動態語言運行時 (DLR) 是 C# 4 和 Microsoft .NET Framework 4 中的重大新增功能。這些功能在宣佈時就引起了人們的極大興趣,並伴隨著許多疑問。同時人們也給出了很多答案,但這些答案現在已散佈于各種文檔以及各種技術博客和文章之中。這樣,人們在各種論壇和會議上總是一遍又一遍地提出相同的問題。

本文全面概述了 C# 4 中新增的動態功能,並且深入探討了這些功能如何同其他語言和框架功能(例如反射或隱式類型化變數)一起使用。鑒於已有大量資訊可用,我有時會重新使用一些經典示例,並提供指向原始源的連結。我還將提供指向相關內容的大量連結,供您進一步閱讀。

什麼是“動態”?

程式設計語言有時可劃分為靜態類型化語言和動態類型化語言。C# 和 Java 經常被認為是靜態類型化語言的例子,而 Python、Ruby 和 JavaScript 是動態類型化語言的例子。

一般而言,動態語言不執行編譯時類型檢查,僅在運行時識別物件的類型。這種方法有利有弊:代碼編寫起來往往更快、更容易,但同時,由於您不會獲得編譯器錯誤,只能通過單元測試和其他方法來確保應用程式正常運行。

C# 最初是作為純靜態語言創建的,但 C# 4 添加了一些動態元素,用以改進與動態語言和框架之間的互通性。C# 團隊考慮了多種設計選項,但最終確定添加一個新關鍵字來支援這些功能:dynamic。

dynamic 關鍵字可充當 C# 類型系統中的靜態型別宣告。這樣,C# 就獲得了動態功能,同時仍然作為靜態類型化語言而存在。若要瞭解為何以及如何做出了這樣的決定,請參考 PDC09 (microsoftpdc.com/2009/FT31) 上由 Mads Torgersen 撰寫的演示文稿“C# 4 中的動態繫結”。尤其是,動態物件被認定是 C# 語言中的“一等公民”,因此沒有用於打開或關閉動態功能的選項,並且沒有向 C# 添加過類似于 Visual Basic 中的 Option Strict On/Off 之類的功能。

當您使用 dynamic 關鍵字時,您就告訴編譯器關閉編譯時檢查。網上以及 MSDN 文檔中 (msdn.microsoft.com/library/dd264736) 有大量關於如何使用該關鍵字的示例。下麵是一個常見示例:

dynamic d = "test";
Console.WriteLine(d.GetType());
// Prints "System.String".
d = 100;
Console.WriteLine(d.GetType());
// Prints "System.Int32".

如您所見,可以將不同類型的物件分配給已聲明為 dynamic 的變數。 這段代碼會通過編譯,並在運行時確定物件的類型。 不過,下麵的代碼也會通過編譯,但在運行時會引發異常:

dynamic d = "test";

// The following line throws an exception at run time.
d++;

原因是相同的:編譯器不知道該物件的運行時類型,因此無法告訴您遞增操作在此情況下不受支援。

缺少編譯時類型檢查也會導致 IntelliSense 功能無效。 由於 C# 編譯器不知道物件的類型,因此它無法枚舉該物件的屬性和方法。 正如在用於 Visual Studio 的 IronPython 工具中那樣,通過附加的類型推斷可能會解決此問題,但目前 C# 不提供這種類型推斷。

但是,在許多可能獲益于動態功能的方案中,由於代碼使用了字串文本而導致 IntelliSense 還是不可用。 本文在後面將對這一問題進行更詳細的討論。

Dynamic、Object 還是 Var?

那麼,dynamic、object 和 var 之間的實際區別是什麼?何時應使用它們? 下麵是每個關鍵字的簡短定義和一些示例。

關鍵字 object 表示 System.Object 類型,它是 C# 類層次結構中的根類型。 此關鍵字經常在編譯時無法確定物件類型時使用,而這種情況經常在各種互通性情形中發生。

您需要使用顯式轉換將已聲明為 object 的變數轉換為特定類型:

object objExample = 10;
Console.WriteLine(objExample.GetType());

顯然,這將輸出 System.Int32。 但是,因為靜態類型為 System.Object,所以您在這裡需要一個顯式轉換:

objExample = (int)objExample + 10;

您可以賦予不同類型的值,因為它們都是從 System.Object 繼承的:

objExample = "test";

從 C# 3.0 起,關鍵字 var 開始用於隱式類型化區域變數以及匿名類型。 此關鍵字經常與 LINQ 結合使用。 當使用 var 關鍵字聲明變數時,將在編譯時根據初始化字串推斷該變數的類型。 在運行時無法更改該變數的類型。 如果編譯器不能推斷類型,它會生成一個編譯錯誤:

var varExample = 10;
Console.WriteLine(varExample.GetType());

這段代碼會輸出 System.Int32,與靜態類型相同。

在下麵的示例中,因為 varExample 的靜態類型為 System.Int32,所以不需要轉換:

varExample = varExample + 10;

下麵一行不進行編譯,因為只能將整數賦給 varExample:

varExample = "test";

C# 4 中引入的 dynamic 關鍵字可使某些傳統上依賴于 object 關鍵字的情形更容易編寫和維護。 實際上,動態類型在後臺使用 System.Object 類型。但與 object 不同的是,動態類型不需要在編譯時執行顯式轉換操作,因為它僅在運行時識別類型:

dynamic dynamicExample = 10;
Console.WriteLine(dynamicExample.GetType());

此段代碼會輸出 System.Int32。

在下麵這一行中不需要轉換,因為僅在運行時識別類型:

dynamicExample = dynamicExample + 10;

可以將不同類型的值賦給 dynamicExample:

dynamicExample = "test";

在 C# 常見問題解答博客 (bit.ly/c95hpl) 上,提供了關於關鍵字 object 和 dynamic 之間差別的詳細博客文章。

有時會引起混淆的是,所有這些關鍵字可以一起使用,即它們不是互相排斥的。 例如,我們來看一看下麵的代碼:

dynamic dynamicObject = new Object();
var anotherObject = dynamicObject;

anotherObject 的類型是什麼? 我的回答是:dynamic。 請記住,在 C# 類型系統中,dynamic 實際上是一個靜態類型,因此,編譯器將為 anotherObject 推斷此類型。 務必要知道,var 關鍵字不過是一個指令,它讓編譯器根據變數的初始設定式推斷類型;var 不是類型。

動態語言運行時

說起 C# 語言環境中的“dynamic”這一術語,它通常指下麵兩個概念之一:C# 4 中的 dynamic 關鍵字或 DLR。 雖然這兩個概念是相關的,但也務必要瞭解它們之間的差別。

DLR 有兩個主要目的。 首先,它實現動態語言和 .NET Framework 之間的交互操作。 其次,它將動態行為引入 C# 和 Visual Basic 之中。

DLR 的創建吸取了構建 IronPython (ironpython.net) 時的經驗教訓(IronPython 是在 .NET Framework 上實現的第一種動態語言)。 在構建 IronPython 時,工作團隊發現他們可以針對多種語言重複使用他們的實現,因此,他們為 .NET 動態語言創建了一個公共基礎平臺。 與 IronPython 一樣,DLR 已成為一個開源專案,其原始程式碼目前在dlr.codeplex.com 上提供。

後來,.NET Framework 4 中也納入了 DLR,以支援 C# 和 Visual Basic 中的動態功能。 如果您只需要 C# 4 中的 dynamic 關鍵字,那麼使用 .NET Framework 就可以了。在大多數情況下,僅憑 .NET Framework 即可處理與 DLR 之間的所有交互。 但是,如果您希望實現新的動態語言或將其遷移到 .NET,則可以獲益于開源專案中額外的説明程式類,該開源專案為語言實現人員提供了更多功能和服務。

在靜態類型化語言中使用 Dynamic

我們並不期待每個人都盡可能使用動態而不是靜態型別宣告。 編譯時檢查是一個強大的工具,對它的使用多多益善。 而且,再次指出,C# 中的動態物件不支援 IntelliSense,這對總體工作效率可能會有些影響。

同時,在出現 dynamic 關鍵字和 DLR 之前,有一些方案在 C# 中曾經難以實現。 在以前的大多數情況下,開發人員使用 System.Object 類型和顯式轉換,同樣不能很好地利用編譯時檢查和 IntelliSense。 下麵是一些例子。

人們最熟知的一個情況是,有時必須使用 object 關鍵字來實現與其他語言或框架的互通性。 通常,您必須依靠反射來獲取物件的類型以及訪問其屬性和方法。 語法有時難以閱讀,因此代碼難以維護。 此時使用動態功能可能比使用反射更加容易和方便。

Anders Hejlsberg 在 PDC08 (channel9.msdn.com/pdc2008/TL16) 上提供了一個極好的例子,如下所示:

object calc = GetCalculator();
Type calcType = calc.GetType();
object res = calcType.InvokeMember(
  "Add", BindingFlags.InvokeMethod, 
  null, new object[] { 10, 20 });
int sum = Convert.ToInt32(res);

該函數返回一個計算器,但系統在編譯時不知道此計算器物件的精確類型。 代碼所依賴的唯一事情是此物件應具有 Add 方法。 請注意,此方法無法使用 IntelliSense,因為您以字串文本的形式提供了方法名稱。

使用 dynamic 關鍵字,代碼就很簡單了:

dynamic calc = GetCalculator();
int sum = calc.Add(10, 20);

假設情況沒有變化:存在某種我們希望其具有 Add 方法的未知類型的物件。 與上一個示例一樣,此方法也不能使用 IntelliSense。 但語法閱讀和使用起來要容易很多,看上去就像在調用一個普通的 .NET 方法。

動態方法包

可以利用動態功能的另外一個例子是創建動態方法包,動態方法包就是可在運行時添加和刪除屬性及方法的物件。

.NET Framework 4 有一個新的命名空間:System.Dynamic。 此命名空間實際上是 DLR 的一部分。 System.Dynamic.ExpandoObject 和 System.Expando.DynamicObject 類與新的 dynamic 關鍵字相結合,有助於以清晰和易於閱讀的方式來創建動態結構和層次結構。

例如,下麵說明了如何使用 ExpandoObject 類來添加屬性和方法:

dynamic expando = new ExpandoObject();
expando.SampleProperty = 
  "This property was added at run time";
expando.SampleMethod = (Action)(
  () => Console.WriteLine(expando.SampleProperty));
expando.SampleMethod();

要瞭解更加深入的方案,您一定要看看關於 ExpandoObject 和 DynamicObject 類的 MSDN 文檔。 同時,還有一些值得一看的文章,比如由 Bill Wagner 撰寫的文章“動態方法包”(msdn.microsoft.com/library/ee658247) 以及 C# 常見問題解答博客文章“C# 4.0 中的 Dynamic:ExpandoObject 簡介”(bit.ly/amRYRw)。

類包裝

您可以為自己的庫提供更好的語法,或為現有庫創建包裝。 與前兩個方案相比,這是一個更高級的方案,並且需要對 DLR 具體內容有更深入的瞭解。

對於簡單情況,可以使用 DynamicObject 類。 在這個類中,可以將方法和屬性的靜態聲明與動態調度進行混合。 這樣,您就可以在一個類屬性中存儲一個要為其提供更佳語法的物件,但通過動態調度來處理針對該物件的所有操作。

例如,請看一下圖 1 中的 DynamicString 類,該類包裝了一個字串,並在通過反射實際調用所有方法之前顯示這些方法的名稱。

圖 1 DynamicString

public class DynamicString : DynamicObject {
  string str;

  public DynamicString(string str) {
    this.str = str;
  }

  public override bool TryInvokeMember(
    InvokeMemberBinder binder, object[] args, 
    out object result) {

    Console.WriteLine("Calling method: {0}", binder.Name);

    try {
      result = typeof(string).InvokeMember(
        binder.Name,
        BindingFlags.InvokeMethod |
        BindingFlags.Public |
        BindingFlags.Instance,
        null, str, args);
      return true;
    }
    catch {
      result = null;
      return false;
    }
  }
}

若要產生實體該類,應使用 dynamic 關鍵字:

dynamic dStr = new DynamicString("Test");
Console.WriteLine(dStr.ToUpper()); 
Console.ReadLine();

當然,這個特定示例出於演示目的而設計,不具有實際效率。但是,如果您擁有已嚴重依賴于反射的 API,就可以如此處所示將所有通過反射進行的調用打包,以便針對 API 的最終使用者隱藏這些調用。

有關更多示例,請參見 MSDN 文檔 (msdn.microsoft.com/library/system.dynamic.dynamicobject) 以及 C# 常見問題解答博客文章“C# 4.0 中的 Dynamic:通過 DynamicObject 創建包裝”(bit.ly/dgS3od)。

如前所述,DynamicObject 類是由 DLR 提供的。生成動態物件所需要的僅僅是 DynamicObject 或 ExpandoObject。但是,某些動態物件具有用於訪問成員和調用方法的複雜綁定邏輯。這種物件需要實現 IDynamicMetaObjectProvider 介面並提供其自己的動態調度。這是一種高級方案,感興趣的讀者可以讀一下由 Bill Wagner 撰寫的文章“實現動態介面”(msdn.microsoft.com/vcsharp/ff800651),以及由 Alex Turner 及 Bill Chiles 撰寫的文章“庫創作者 DLR 入門”(dlr.codeplex.com)。

可編寫腳本的應用程式

腳本是向應用程式提供可擴展性的一種強大方法。Microsoft Office 可作為這方面的一個好例子:由於 Visual Basic for Applications (VBA) 的存在,可以使用大量的宏、載入項和外掛程式。現在,DLR 提供了一組公用的語言宿主 API,因此可讓您創建可編寫腳本的應用程式。

例如,您可以創建一個應用程式,使使用者能夠自己在其中添加功能而不需要主產品提供新功能,例如向遊戲中添加新的字元和映射,或向業務應用程式添加新的圖表。

您必須使用來自 dlr.codeplex.com 的開源版 DLR 而不是由 .NET Framework 4 使用的 DLR,因為 DLR 腳本編寫和宿主 API 現在僅在開源版中提供。另外,假定的情況是您不是使用 C# 編寫腳本,而是使用一種 .NET 動態語言(如 IronPython 或 IronRuby)來編寫。然而,實際上任何語言都可以支援這些 API,包括不是在 DLR 之上實現的語言。

有關使用此功能的詳細資訊,請觀看 PDC09 (microsoftpdc.com/2009/FT30) 上由 Dino Viehland 所做的演示“使用動態語言生成可編寫腳本的應用程式”。

識別動態物件

如何區分動態物件與其他物件?一個簡便方法是使用內置的 IDE 功能。您可以將滑鼠游標懸停在物件上以查看其聲明類型,或檢查 IntelliSense 是否可用(請參見圖 2)。

圖 2 Visual Studio 中的動態物件

然而在運行時,情況會變得更加複雜。您無法檢查變數是否是通過 dynamic 關鍵字聲明的 — 動態物件的運行時類型是它所存儲的值的類型,您無法獲取其靜態型別宣告。這種情況與將變數聲明為 object 時的情況相同:在運行時,您只能獲取變數所存儲的值的類型;無法判斷此變數最初是否聲明為 object。

運行時所能確定的是物件是否來自 DLR。知道這種情況可能十分重要,因為像 ExpandoObject 和 DynamicObject 類型的物件可在運行時改變其行為,例如,添加和刪除屬性及方法。

此外,也無法使用標準反射方法來獲取有關這些物件的資訊。如果向 ExpandoObject 類的實例添加屬性,則無法通過反射獲取該屬性:

dynamic expando = new ExpandoObject();
expando.SampleProperty = "This property was added at run time";
PropertyInfo dynamicProperty = 
  expando.GetType().GetProperty("SampleProperty");
// dynamicProperty is null.

有利的方面是,在 .NET Framework 4 中,所有可動態添加和刪除成員的物件都必須實現一個特定介面:System.Dynamic.IDynamicMetaObjectProvider。 DynamicObject 和 ExpandoObject 類也實現了這個介面。 不過,這並不表示任何使用 dynamic 關鍵字聲明的物件都實現此介面:

dynamic expando = new ExpandoObject();
Console.WriteLine(expando is IDynamicMetaObjectProvider);
// True

dynamic test = "test";
Console.WriteLine(test is IDynamicMetaObjectProvider);
// False

因此,如果將動態功能與反射一起使用,則請記住,反射不適用於動態添加的屬性和方法,並且最好檢查正在反射的物件是否實現了 IDynamicMetaObjectProvider 介面。

動態功能與 COM 交互操作

C# 團隊在 C# 4 版本中專門考慮的 COM 交互操作方案是針對 Microsoft Office 應用程式(如 Word 和 Excel)進行程式設計。 他們的目的是讓這一任務在 C# 中變得像在 Visual Basic 中那樣容易和自然。 這也是 Visual Basic 和 C# 共同發展策略的一部分,這個策略旨在實現兩種語言的功能對等,並相互借鑒最佳、最具效率的解決方案。

若需瞭解詳細資訊,請參閱 Scott Wiltamuth 的 Visual Studio 博客文章“C# 和 VB 共同發展”(bit.ly/bFUpxG)。

圖 3 顯示了一段 C# 4 代碼,該代碼向 Excel 工作表的第一個儲存格中添加一個值,然後向第一列應用 AutoFit 方法。 每行下麵的注釋顯示了 C# 3.0 及更早版本的中的等效代碼。

圖 3 通過 C# 對 Excel 進行腳本程式設計

// Add this line to the beginning of the file:
// using Excel = Microsoft.Office.Interop.Excel;

var excelApp = new Excel.Application();

excelApp.Workbooks.Add();
// excelApp.Workbooks.Add(Type.Missing);

excelApp.Visible = true;

Excel.Range targetRange = excelApp.Range["A1"];
// Excel.Range targetRange = excelApp.get_Range("A1", Type.Missing);

targetRange.Value = "Name";
// targetRange.set_Value(Type.Missing, "Name");

targetRange.Columns[1].AutoFit();
// ((Excel.Range)targetRange.Columns[1, Type.Missing]).AutoFit();

此示例有趣的地方是,您在代碼中的任何位置都看不到 dynamic 關鍵字。實際上,該關鍵字僅在下麵一行中用到:

targetRange.Columns[1].AutoFit();
// ((Excel.Range)targetRange.Columns[1, Type.Missing]).AutoFit();

在 C# 3.0 版中,targetRange.Columns[1, Type.Missing] 返回 object,這便是需要向 Excel.Range 轉換的原因。但在 C# 4 和 Visual Studio 2010 中,這樣的調用將以靜默方式轉換為動態調用。因此,C# 4 中 targetRange.Columns[1] 的類型實際上是 dynamic。

另一個突出特點是,C# 4 中的 COM 交互操作改進不僅限於 dynamic。由於其他一些新增功能(例如索引屬性以及具名引數和可選參數),其他所有行中的代碼也有所改進。由 Chris Burrows 撰寫的 MSDN 雜誌 文章“.NET Framework 4 中的新增 C# 功能”(msdn.microsoft.com/magazine/ff796223) 中對這些新增功能做了很好的概述。

從哪裡可以獲取更多資訊?

希望本文已涵蓋您對 C# 4 中的 dynamic 關鍵字可能有的大部分疑問,但我確信本文並非面面俱到。如果您有意見、問題或建議,敬請光臨 dlr.codeplex.com/discussions 提出。其他人可能已經提過您關心的問題,或者您可以發起新的討論。我們擁有一個活躍的社區,歡迎新成員加入。

Alexandra Rusina 是 Silverlight 團隊的一名專案經理。在此之前,她曾于 Visual Studio 2010 發佈期間在 Visual Studio 語言團隊擔任程式師。她還定期在 C# 常見問題解答博客 (blogs.msdn.com/b/csharpfaq/) 上發表博文。

衷心感謝以下技術專家對本文的審閱: Bill Chiles