C# 4.0的主要主題是動態編程。對象的意義變得越來越“動態”,它們的結構和行為無法通過靜態類型來捕獲,或者至少編譯器在編譯程序時無法得知對象的結構和行為。
a. 來自動態編程語言——如Python或Ruby——的對象
b. 通過IDispatch訪問的COM對象
c. 通過反射訪問的一般.NET類型
d. 結構發生過變化的對象——如HTML DOM對象
C# 4.0中的新特性分為四組——
動態查找
動態查找允許在編寫方法、運算符和索引器調用、屬性和字段訪問甚至對象調用時,繞過C#靜態類型檢查,而在運行時進行解析。
命名參數和可選參數
現在C#中的參數可以通過在成員聲明中為其提供默認值來指名它是可選的。在調用該成員時,可選參數可以忽略。另外,在傳入任何參數時都可以按照參數名而不是位置進行傳遞。
特定於COM的互操作特性
動態查找以及命名參數和可選參數都有助於使針對COM的編程不再像今天這樣痛苦。在這些特性之上,我們還添加了大量其他小特性,進一步改善了互操作體驗。
變性
過去,IEnumerable<string>並不是IEnumerable<object>。現在它是了——C#包含了類型安全的“協變性和逆變性(co-and contravariance)”而且通用的BCL也將利用這一特性進行更新
動態查找
動態查找可以用統一的方式來動態調用成員。有了動態查找,當你拿到一個對象時,不用管它是來自於COM還是IronPython、HTML DOM或是反射;只需要對其進行操作即可,運行時會幫你指出針對特定的對象,這些操作的具體意義。
C#的一個設計目標就是允許在每個單獨的調用中選擇是否使用動態行為。
類型
C# 4.0引入了一個新的靜態類型,稱為dynamic。當你擁有了一個dynamic類型的對象后,你“對他做的事情”只會在運行時進行解析——
dynamic d = GetDynamicObject(...); d.M(7);
C#編譯器允許你使用任何參數在d上調用一個方法,因為它的類型是dynamic。運行時會檢查d的實際類型,並檢測在它上面“用一個int調用M”是什么意思。
可以認為dynamic類型是object類型的一個特殊版本,指出了對象可以動態地使用。選擇是否使用動態行為很簡單——任何對象都可以隱式轉換為dynamic,“掛起信任”直到運行時。反之,從dynamic到任何其他類型都存在“賦值轉換”,可以類似於賦值的結構中進行隱式轉換——
1.dynamic d = 7; // implicit conversion 2.int i = d; // assignment conversion
動態操作
不僅是方法調用,字段和屬性訪問、索引器和運算符調用甚至委托調用都可以動態地分派——
dynamic d = GetDynamicObject(…);
d.M(7); // calling methods
d.f = d.P; // getting and settings fields and properties
d[“one”] = d[“two”]; // getting and setting thorugh indexers
int i = d + 3; // calling operators
string s = d(5,7); // invoking as a delegate
C#編譯器在這里的角色就是打包有關“在d上做什么”的必要信息,使得運行時可以獲取這些信息並檢測對於實際對象d這些操作的確切含義。可以認為這是將編譯器的部分工作延遲到了運行時。
任何動態操作的結果本身也是dynamic類型的
運行時查找
在運行時,動態操作將根據目標對象d的本質進行分派——
COM對象
如果d是一個COM對象,則操作通過COM IDispatch進行動態分派。這允許調用沒有主互操作程序集(Primary Interop Assembly,PIA)的COM類型,並依賴C#中沒有對應概念的COM特性,如索引屬性和默認屬性。
動態對象
如果d實現了IDynamicObject接口,則請求d自身來執行該操作。因此通過實現IDynamicObject接口,類型可以完全重新定義動態操作的意義。這在動態語言——如IronPython和IronRuby——中大量使用,用於實現他們的動態對象模型。API也會使用這類對象,例如HTML DOM允許直接使用屬性語法來訪問對象的屬性。
實例:
dynamic d1 = new Foo(); dynamic d2 = new Bar(); string s; d1.M(s, d2, 3, null);
由於對M進行調用的接受者是dynamic類型的,C#編譯器不會試圖解析該調用的意義。而是將有關該調用的信息存儲起來,供運行時使用。該信息(通常稱作“有效載荷”)本質上等價於
——
“使用下面的參數執行一個稱作M的實例方法——
- 1. 一個string
- 2. 一個dynamic
- 3. 一個int字面值3
- 4. 一個object字面值null”
在運行時,假設d1的實際類型Foo不是COM類型,也沒有實現IDynamicObject。在這種情況下,C#運行時綁定器擔負起了重載解析的工作,這是基於運行時類型信息完成的,按照下面的步驟進行處理——
- 1. 使用反射獲取兩個對象d1和d2的實際運行時類型,它們沒有靜態類型(包括靜態類型dynamic)。結果為d1是Foo類型而d2是Bar。
- 2. 使用普通的C#語義在Foo類型上對M(string,Bar,3,null)調用進行方法查找和重載解析。
- 3. 如果找到了該方法,則調用它;否則拋出運行時異常。
帶有動態參數的重載解析
即便方法調用的接受者是靜態類型的,重載解析依然發生在運行時。當一個或多個實參是dynamic類型時就會出現這種情況
Foo foo = new Foo(); dynamic d = new Bar(); var result = foo.M(d);
C#運行時綁定器會基於d的運行時類型——也就是Bar——在Foo上M方法的靜態可知(statically known)重載之間進行選擇。其結果是dynamc類型。
動態語言運行時
動態語言運行時(Dynamic Language Runtime,DLR)是動態查找的底層實現的一個重要組件,也是.NET 4.0中新增的API。
DLR不僅為C#動態查找,還為很多其他.NET上的動態語言——如IronPython和IronRuby——的實現提供了底層的基礎設施。這一通用基礎設施確保了高度的互操作性,更重要的是,DLR提供了卓越的緩存機制,使得運行時分派的效率得到巨大的改善。
對於使用C#動態查找的用戶來說,除了更高的性能之外,根本感覺不到DLR的存在。不過,如果你希望實現自己的動態分派對象,可以使用IDynamicObject接口來與DLR互操作,並向其中插入自己的行為。
已知問題
這里可能有一些限制或與你期望的結果不同。
- DLR允許從一個表示類的對象創建對象。然而,C#的當前實現還不具備支持這一功能的語法。
- 動態查找不能查找擴展方法。不論擴展方法是否依賴該調用的靜態上下文(也就是出現了using語句),因為該上下文信息並不會作為有效載荷的一部分保留下來。
- 匿名函數(也就是lambda表達式)不能作為實參傳遞給動態方法調用。在不知道要轉換成什么類型的情況下,編譯器不能綁定(也就是“理解”)一個匿名函數。
這些限制導致的結果就是很難在動態對象上使用LINQ查詢——
var result = collection.Select(e => e + 5); dynamic collection = ...;
如果Selected方法是個擴展方法,動態查找將找不到它。即便它是一個實例方法,上面的代碼也無法編譯,因為lambda表達式不能作為參數傳遞給動態操作。
命名參數和可選參數
命名參數和可選參數是兩個截然不同的功能,但通常一起使用。在進行成員調用時,可以忽略可選參數;而命名參數的方式可以通過名稱來提供一個參數,而無需依賴它在參數列表中出現的位置。
有些API——尤其是COM接口——如Office自動化API——確實本身就是通過命名參數和可選參數編寫的。之前在C#中調用這些API非常痛苦,尤其有的時候需要多達30幾個參數都必須顯式傳遞,而其中大多數都具有合理的默認值,是可以忽略的。
即便是編寫.NET中的API,你也會發現很多時候你在被迫為不同的參數組合方式編寫一個方法的大量重載形式,以便給調用者提供最高的可用性。在這種情況下,可選參數就會成為一種非常有用的替代方式。
可選參數
為一個參數提供默認值就可以將其聲明為可選的——
public void M(int x, int y = 5, int z = 7);
這里的y和z就是可選參數,在調用時可以忽略——
M(1, 2, 3); // ordinary call of M M(1, 2); // omitting z – equivalent to M(1, 2, 7) M(1); // omitting both y and z – equivalent to M(1, 5, 7)
命名的和可選的實參
C# 4.0不允許忽略逗號之間的實參,比如M(1,,3)。否則會導致大量不可讀的、需要“數逗號”的代碼。替代方式是任何參數都可以通過名字傳遞。因此如果在調用M時只希望忽略y,可以寫——
- M(1, z: 3); // passing z by name
或
- M(x: 1, z: 3); // passing both x and z by name
甚至
- M(z: 3, x: 1); // reversing the order of arguments
這幾種形式都是等價的,不過參數總是按照其出現的順序進行求值,因此對於最后一個示例來說,3會在1之前求值。
可選參數和命名參數不僅可以用在方法調用中,還可以用在索引器和構造器中。
重載解析
命名參數和可選參數影響了重載解析,但產生的變化相當簡單——
如果所有的參數或者是可選的,或者在調用時(通過名字或位置)明確提供了對應的實參,並且實參能夠轉換為形參類型,則該簽名是可適用的(applicable)。
轉換的最優原則只用於明確給定的實參——出於最優的目的,忽略掉的可選參數在重載解析時將不做考慮。
如果兩個簽名一樣好,則沒有忽略可選參數的那個勝出。
M(string s, int i = 1); M(object o); M(int i, string s = “Hello”); M(int i); M(5);
M(int,string)和M(int)都比M(object)要好,因為將5轉換為int優於將5轉換為object。
最后,M(int)優於M(int,string),因為它沒有被忽略的可選參數。
因此,最終調用的方法是M(int)。
COM互操作特性
動態查找以及命名參數和可選參數極大地改善了與COM API——如Office Automation API——互操作的體驗。為了減少更多的速度損失,C# 4.0還添加了大量特定於COM的小特性。
動態導入:
很多COM方法接受並返回可變類型,這在PIA中會表現為object。在絕大多數情況下,程序員在調用這些方法之前就已經從上下文中知道了一個返回值對象的靜態類型,但為了使用這些知識,必須明確地在返回值上進行類型轉換。這些轉換非常普遍,帶來了巨大的麻煩。
為了得到無縫體驗,現在你可以選擇使用dynamic類型來代替可變類型的方式。換句話說,從你的角度來看,COM簽名中出現的是dynamic而不是object。
這意味着你可以直接在返回的對象上訪問成員,或者可以使用強類型的局部變量為其賦值,而無需進行轉換。例如,你可以寫
excel.Cells[1, 1].Value = "Hello";
而不用寫
((Excel.Range)excel.Cells[1, 1]).Value2= "Hello";
又如
Excel.Range range = excel.Cells[1, 1];
而不用寫
Excel.Range range = (Excel.Range)excel.Cells[1, 1];
無PIA的編譯
主互操作程序集(Primary Interop Assembly)是從COM接口生成的大型.NET程序集,用於協助完成強類型的互操作。它們為設計時提供了巨大的支持,就好像其中的類型真的是 用.NET定義的一樣。然而,在運行時這些大型程序集很容易使你的程序膨脹起來,而且很容易導致版本問題,因為它們是分布式的,不依賴你的應用程序。
無PIA特性允許你繼續在設計時使用PIA,而無需在運行時使用它們。C#編譯器會將程序中實際用到的PIA中的一小部分直接編譯到程序集中。在運行時無需加載PIA。
省略ref
由於采用了不同的編程模型,很多COM API包含大量的引用參數。與C#中的ref相反,這些參數並不意味着要修改傳入的實參以供調用方之后使用,而只是另外一種傳遞參數值的簡單方式。
C#程序員必須為所有這些ref參 數創建臨時變量,並按引用進行傳遞,這看上去一點也不合理。因此,對於COM方法,C#編譯器允許按值傳遞這些參數,並自動生成存放傳入值的臨時變量,並 在調用返回后丟棄這些變量。使用這種方式,調用方看到的語義是按值傳遞,而且不會有任何副作用,而被調用的方法得到的依然是一個引用。
變性
泛型的某個方面會讓人感到奇怪,比如下面的代碼是不合法的——
IList<string> strings = new List<string>(); IList<object> objects = strings;
第二個賦值是不允許的,因為strings和objects的元素類型並不一樣。這樣做有這充分的原因。如果允許那樣寫的話,你可能會寫——
objects[0] = 5; string s = strings[0];
這會允許將int插入strings列表中,然后將其作為string取出。這會破壞類型安全。
然而,對於某些接口來說上述情況並不會發生,尤其是不能將對象插入集合時。例如IEnumerable<T>就是這樣的接口。如果改為——
IEnumerable<object> objects = strings;
這樣就沒法通過objects將錯誤類型的東西插入到strings中了,因為objects沒有插入元素的方法。變性(variance)就是用於在這種能保證安全的情況下進行賦值的。結果就是很多之前讓我們感到奇怪的情況現在可以工作了。
協變性
在.NET 4.0中,IEnumerable<T>接口將會按照下面的方式進行定義——
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IEnumerator
{
bool MoveNext();
T Current { get; }
}
其結果是,任何一個字符串序列也就是一個對象序列了。
這很有用,例如在LINQ方法中。使用上面的定義——
var result = strings.Union(objects); // succeeds with an IEnumerable<object>
之前這樣做是不允許的,你必須做一些麻煩的包裝,使得兩個序列具有相同的元素類型。
類型參數還可以具有“in”修飾符,限制它們只能出現在輸入位置上。例如IComparer<T>—
public interface IComparer<in T>
{
public int Compare(T left, T right);
}
其結果有點讓人迷惑,就是IComparer<object>可以作為IComparer<string>使用!這樣考慮這個結果就會很有意義——如果一個比較器可以比較任意兩個object,它當然也可以比較兩個string。這種性質被稱作“逆變性(contravariance)”。
泛型類型可以同時擁有帶有in和out修飾符的類型參數,例如Func<...>委托類型——
public delegate TResult Func<in TArg, out TResult>(TArg arg);
很明顯參數永遠都是傳入的,而結果永遠只能是傳出的。因此,Func<object, string>可以用作Func<string, object>。
限制
變性類型參數只能在接口和委托類型中聲明,這是CLR的限制。變性只能應用在類型參數的按引用轉換之間。例如,IEnumerable<int>不能作為IEnumerable<object>使用,因為從int到object的轉換是裝箱轉換,而不是引用轉換。
還要注意的是,CTP中並沒有包含前面提到的.NET類型的新版本。為了試驗變性,你需要自己聲明變性接口和委托類型。
COM示例
這里有一個稍大一些的Office自動化示例,展示了大部分C#新特性的實際應用。
using System;
using System.Diagnostics;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
class Program
{
static void Main(string[] args) {
var excel = new Excel.Application();
excel.Visible = true;
excel.Workbooks.Add(); // optional arguments omitted
excel.Cells[1, 1].Value = "Process Name"; // no casts; Value dynamically
excel.Cells[1, 2].Value = "Memory Usage"; // accessed
var processes = Process.GetProcesses()
.OrderByDescending(p => p.WorkingSet)
.Take(10);
int i = 2;
foreach (var p in processes) {
excel.Cells[i, 1].Value = p.ProcessName; // no casts
excel.Cells[i, 2].Value = p.WorkingSet; // no casts
i++;
}
Excel.Range range = excel.Cells[1, 1]; // no casts
Excel.Chart chart = excel.ActiveWorkbook.Charts.
Add(After: excel.ActiveSheet); // named and optional arguments
chart.ChartWizard(
Source: range.CurrentRegion,
Title: "Memory Usage in " + Environment.MachineName); //named+optional
chart.ChartStyle = 45;
chart.CopyPicture(Excel.XlPictureAppearance.xlScreen,
Excel.XlCopyPictureFormat.xlBitmap,
Excel.XlPictureAppearance.xlScreen);
var word = new Word.Application();
word.Visible = true;
word.Documents.Add(); // optional arguments
word.Selection.Paste();
}
}
