俗話說學以致用,本系列的出發點就在於總結C#和C++的一些新特性,並給出實例說明這些新特性的使用場景。前幾篇文章將以C#的新特性為綱領,並同時介紹C++中相似的功能的新特性,最后一篇文章將總結之前幾篇沒有介紹到的C++11的新特性。
C++從11開始被稱為現代C++(Modern C++)語言,開始越來越不像C語言了。就像C#從3.0開始就不再像Java了。這是一種超越,帶來了開發效率的提高。
一種語言的特性一定是與這種語言的類型和運行環境是分不開的,所以文章中說C#的新特性其中也包括新的.NET Framework和CLR(DLR)對C#的支持。
系列文章目錄
3. C#與C++的發展歷程第三 - C#5.0異步編程的巔峰
由於C#2.0除了泛型,迭代器yield,foreach等與Java等有所不同,其它沒有特別之處,所以本系列將直接從C#3.0開始。
C#3.0 (.NET Framework 3.5, CLR 2.0 下同)
C# 對象初始化器與集合初始化器
在對象初始化器出現之前,我們實例化一個對象並賦值的過程代碼看起來是很冗余的。比如有這樣一個類:
class Plant { string Name{get;set;} string Category{get;set;} int ImageId{get;set;} }
實例化並賦值的代碼如下:
Plant peony = new Plant(); Peony.Name = "牡丹"; Peony.Category= "芍葯科"; Peony.ImageId=6;
如果我們需要多次實例化並賦值,為了節省賦值代碼,可以提供一個構造函數:
Plant(string Name,string Category,int ImageId) { Name = name; Category=category; ImageId= imageid; }
這樣就可以直接調用構造函數來實例化一個對象並賦值,代碼相當簡潔:
Plant peony = new Plant("牡丹","芍葯科",6);
如果我們只需要給其中2個屬性賦值,或者類中又增加新的屬性,原來的構造函數可能不能再滿足要求,我們需要提供新的構造函數重載。
現在有了對象初始化器,我們可以使用更簡單的語法來實例化對象並賦值:
Plant peony = new Plant { Name = "牡丹", Category="芍葯科", ImageId= 6 }
我們可以根據需求隨意增加或減少對屬性的賦值。
接着來看看集合初始化器,習慣了對象初始化的語法,集合初始化器是水到渠成的:
List< Plant > plants = new List< Plant > { new Plant { Name = "牡丹", Category = "芍葯科", ImageId =6}, new Plant { Name = "蓮", Category = "蓮科", ImageId =10 }, new Plant { Name = "柳", Category = "楊柳科", ImageId = 12 } };
另一個常用的小伙伴Dictionary<K,V>類的對象也可以用類似的方式實例化:
Dictionary<int, Plant > plants = new Dictionary<int, Plant> { { 11, new Plant { Name = "牡丹", Category = "芍葯科", ImageId =6}}, { 12, new Plant { Name = "蓮", Category = "蓮科", ImageId =10 }}, { 13, new Plant { Name = "柳", Category = "楊柳科", ImageId = 12}} };
使用對象初始化器或集合初始化器時賦值部分調用構造函數的圓括號可以省略,直接以花括號開始屬性賦值即可。
在下文介紹匿名類和隱式類型數組時還會看到對象初始化器和集合初始化器的語法。
注意:對於C#3.0的新特性基本上都可以說是語法糖,因為運行的CLR沒有變,只是編譯器幫我們將簡化的語法編譯成我們之前需要手寫的復雜的方式。
C++11 統一的初始化語法
C++11中統一了初始化對象的語法,這語法與C#的對象初始化器是孿生兄弟,就是一對花括號 – {}。我們由基本類型的初始化說起。
在C++11之前,我們初始化一個int一般寫出這樣:
int i(3);
或
int i = 3;
參見本小節末:初始化和賦值的區別
使用新的初始化語法可以寫為:
int i{3};
同樣char類型對象新的初始化方式:
char c{'x'};
使用賦值的方式下,下面代碼是可以工作的:
int f=5.3;
賦值完成后f的值為5,編譯器進行了窄轉換,而使用新的初始化方式,窄轉換就不會發生,即下面的代碼無法通過編譯:
int f{5.3};//注意,類型不匹配,無法通過編譯
接着看一下類類型的例子:
我們使用與C#部分類型的類:
class Plant { public: Plant(); virtual ~Plant(); string m_Name; string m_Category; unsigned int m_ImageId; protected: private: };
不同於C#使用{}初始化類成員時需要顯式指定類成員名稱,C++類通過定義構造函數來獲知初始化列表中參數的順序。我們可以這樣實現一個Plant類的構造函數,其中冒號開始的語法被稱為"構造函數初始化列表":
Plant::Plant(string _name,string _category,unsigned int _imageId) :m_Name(_name),m_Category(_category),m_ImageId(_imageId) { }
別忘了在頭文件中給新的構造函數重載加個聲明,然后就可以這樣實例化一個Plant對象了:
Plant plant{ "牡丹", "芍葯科", 6};
上面的例子都是在棧上分配的對象,對於堆上分配的對象,也可以使用new關鍵字加上新的初始化方式,如對於前面的Plant類,可以使用這種方式在堆上實例化一個新的對象:
Plant *plant = new Plant{ "牡丹", "芍葯科", 6};
對於struct,不需要實現重載構造就可以使用統一的初始化語法:
struct StPlant { string m_Name; string m_Category; unsigned int m_ImageId; };
可以直接這樣實例化一個StPlant對象:
StPlant stplant{"牡丹", "芍葯科", 6};
在C++中聲明,定義,初始化和賦值有着概念上的大不同,這對於用慣C#這樣不太區分這種概念的語言的同學可能感覺很不理解。下面依次介紹下這幾個概念:
聲明,例如:
extern int i;在類型名前添加一個extern關鍵字表示聲明一個變量,這個變量在其他鏈接的文件中被定義。C++中一個變量可以被聲明很多次但只能被定義一次。
定義:
int i;定義是最常見的,注意,定義的同時也表示聲明了這個變量。
初始化,初始化的方式有兩種:
int i(5); int i = 5;前者是直接初始化,后者是拷貝初始化。這兩者的不同是前者是尋找合適的拷貝/移動構造函數,后者是使用拷貝/移動賦值運算符。C++11后明確引入了右值及移動語意,初始化的性能大大提高。
賦值:
int i; i=5;這樣把定義與賦值分開,則賦值的過程一定是調用拷貝/移動賦值運算符,而不是通過構造函數來完成。
最后看看下面這種寫法:
extern int i = 5;這樣extern會被忽略,這是一個定義(含聲明)及拷貝初始化變量的語句,且這個變量不能被再次定義。
C++11 初始化列表
標准庫中的容器也可以使用統一的初始化方式進行填充:
std::vector<int> vec = {0, 1, 2, 3, 4};
更復雜一點的栗子:
vector<Plant> plants = { { "牡丹", "芍葯科", 6}, { "牡丹", "芍葯科", 6}, { "牡丹", "芍葯科", 6} };
同樣std::map系列容器也可以使用類似的方式初始化:
map<int, Plant> plantsDic = { {1, { "牡丹", "芍葯科", 6}}, {2, { "牡丹", "芍葯科", 6}}, {3, { "牡丹", "芍葯科", 6}} };
C++11對初始化列表支持的背后,一個其關鍵作用的角色就是新版標准庫新增的std::initializer<T>模板類。編譯器可以將{list}語法編譯為std::initializer<T>類的對象。新版庫中的容器也都添加了接收std::initializer<T>類型參數的構造函數重載,所以上面示例的幾種寫法都可以被支持。vector中增加的構造函數形如:
template <typename T> vector::vector(std::initializer_list<T> initList);
我們也可以在自己的函數實現中使用std::initializer<T>作為參數,如下代碼:
注意:使用std::initializer<T>需要#include <initializer_list>
void GetGoodNum(std::initializer_list<double> marks) { unsigned int num = 0; // 統計80分以上學生人數 for_each (marks.begin(), marks.end(), [&num](double& m) { if (m>80) { num++; } }) }
這樣我們就可以向函數傳遞一個{list}列表。
GetGoodNum({100,70.5,93,84,65});
這個例子用到了C++11的lambda表達式,后文有關於這個語法的介紹。
C# 隱式類型、匿名類和隱式類型數組
隱式類型
C#3.0中新增了var關鍵字。使用var關鍵字可以簡化一些比較長,比較復雜不容易記憶的類型名的輸入。不同於Javascript中的var,C#中的var在編譯之后會被替換為原有的類型,所以C#中var還是強類型的。
舉幾個簡化我們輸入的例子吧。Tuple是一個比較復雜的泛型類(下篇文章會有介紹),如果沒有var,我們實例化一個Tuple對象的代碼就像:
Tuple<string,string,int> plant = Tuple.Create("蓮","蓮科",1);
使用var代碼就可以簡化為:
var plant = Tuple.Create("蓮","蓮科",1);
在foreach循環中也常常是var的用武之地
foreach(var kvp in dictionaryObj) {}
如果沒有var,我們就要手寫KeyValuePair<K,V>類型的名稱,如果遍歷的集合類是一個不常見的類型,諸如Enumerable.ToLookup()和Enumerable.GroupBy()方法返回的值,可能都記不清其中每一項的具體類型。使用var就可以輕松表示這一切。
var和后面要介紹的Linq也是結合最緊密的,一般Linq返回的都是一個類型非常復雜的對象。使用var能減少很大的編碼工作量,使代碼保持整潔。
匿名類
如果我們將前文介紹的對象初始化器語法中的new關鍵字類型去掉,這樣就得到了匿名類,如:
var peony = new { Name = "牡丹", Category="芍葯科", ImageId= 6 }
匿名類中所有屬性都是只讀的,且其類型都是自動推導得來不能手動指定。當兩個匿名類型具有相同的屬性,則它們被認為是同一個匿名類型
如這個對象:
var peach = new { Name = "桃花", Category = "薔薇科", ImageId = 7, };
判斷類型的話,它們是相同的:
var sametype = peony.GetType() == peach.GetType();
匿名類型的屬性可以直接用另一個對象的屬性來初始化:
var football = new { Name = "足球", Size = "Big", peach.ImageId, };
這樣football中就會有一個名為ImageId的屬性,且值為7。當然也可以自定義名稱,如果屬性名相同省略就好。這種用法在LINQ的Select擴展方法中接收的lambda表達式創建新的匿名對象時常常會見到。
隱式類型數組
通過隱式類型數組這個特性,聲明並初始化數組時也不用顯式指定數組類型了。編譯器會自動推導數組的類型,如:
一維數組
var a = new[] { 1, 10, 100, 1000 }; // int[] var b = new[] { "hello", null, "。world" }; // string[]
交錯數組
var d = new[] { new[]{"Luca", "Mads", "Luke", "Dinesh"}, new[]{"Karen", "Suma", "Frances"} };
隱式類型數組也可以包含匿名類對象,當然所有的匿名類對象要符合同一個匿名類的定義。
參考下面這段示例代碼:
var plants = new[] { new { Name = "蓮", Categories= new[] { "山龍眼目", "蓮科","蓮屬" } }, new { Name = " 柳", PhoneNumbers = new[] { "金虎尾目","楊柳科","柳屬" } } };
C++11 類型推導
在C++中同樣由於模板類型的大量使用導致某些類型的對象的類型不容易記憶及書寫。C++11提供了auto關鍵字來解決這個問題。auto關鍵字的用法與C#中的var極為相似,即在需要指定具體類型的地方代之以auto關鍵字,如方法返回值前,以范圍為基礎的for循環中。
如:
string s("some lower case words"); for(auto it=s.begin(); it!=s.end && !isspace(*it); ++it); *it=toupper(*it); //轉換為大些字母
在C++11中還有一個更為強大的定義類型的操作符 - decltype。我們直接看一個例子,再來說明這個關鍵字的用法:
string s("some words"); decltype(s.size()) index=0;
代碼中s.size()返回值的類型為string::size_type,decltype使用這個類型來定義index變量,代碼中第二句相當於:
string::size_type index=0;
通過decltype可以簡化很多類型的記憶及書寫,編譯器將在編譯時自動以正確的類型替換。
關於decltype更詳細的討論,推薦學習C++ Primer(第5版)2.5.3節內容,其中講述的decltype和引用的問題尤其值得認真學習。
C# 擴展方法
C#的擴展方法主要是為已存在,且不能或不方便直接修改的其代碼的類添加方法。比如,C#3.0中為實現IEnumerable<T>接口的類型添加了如Where,Select等一些列擴展方法,從而可以以Fluent API的方法實現與LINQ等價的功能。這樣,除了一些復雜的如join等通過LINQ語法實現更方便外,其他一些如簡單的where通過Where擴展方法來完成則會使代碼有更好的可讀性。
怎樣實現擴展方法?還是通過一個例子來介紹更直觀:
在寫代碼時我們常遇到需要將一個集合以指定分隔符合並成一個字符串,即String.Join()方法完成的功能。一般的寫法如下:
var list = new List<int>() {1,2,3,4,5}; list = list.Where(i=>i%2==0).ToList(); var str = string.Join(";", list);
可能你會想如果能在第二行代碼一次生成字符串可能更方便,我們通過擴展IEnumerable<T>來實現這個功能:
public static class EnumerableExt { public static string StrJoin<T>(this IEnumerable<T> enumerable, string spliter) { return string.Join(spliter, enumerable); } }
可以看到擴展方法需要定義在靜態類中,且擴展方法自身也需要是靜態方法。擴展方法所在的類的名字不重要,相對而言這個類所在的命名空間的名字更重要,因為是通過引用的命名空間讓編譯器知道我們擴展方法來自於哪里。擴展方法最重要的部分為第一個參數,這個參數前面有一個this,表示我們要擴展這個參數的類型,擴展方法主要執行在這個參數對象上。除此之外實現擴展方法和實現一般方法相同。使用這個擴展方法重寫之前的代碼后:
var list = new List<int>() {1,2,3,4,5}; var str = list.Where(i=>i%2==0).StrJoin(",");
當然這個擴展方法不滿足Fluent API傳入參數和返回值類型相同的要求,但作為調用鏈最后一個方法未嘗不可。
擴展方法這個特性C++沒有類似功能,沒得寫。
C# Lambda表達式
在lambda表達式出現之前,只能通過委托表示一個函數,通過委托的實例或匿名函數來表示一個“函數對象”。有了lambda表達式,C#2.0中出現的匿名函數就可以退役了。lambda表達式可以完全取代匿名函數實現的功能。而且.NET Framework新增的Action及Func<T>系列委托類型也可以減少我們自定義委托類型的必要。
C#的lambda表達式的語法概括如下:
參數部分 => 方法體
對於參數部分,如果有2個或2個以上的參數需要用小括號括起來,lambda表達式的參數部分參數無需指定類型,編譯器會自動進行類型推導。當然也可以明確指定參數類型:
(int x) => x+1;
對於方法體部分如果只有一條語句則無需加{},且對於有返回值的方法體也可以省略return關鍵字。如果是超過一條語句則需要{}且對於有返回值的情況不能省略return,如:
x => { x=x+1; return Math.Pow(x,2);}
C#中lambda表達式一般用於各種和委托類型相關的場景,比如一個方法接收委托類型參數或返回一個委托類型對象。在實現Fluent API樣式的LINQ語法的那些擴展方法中很多都是接收委托類型的參數,如:
IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
調用這些方法時,相應的參數傳入lambda表達式就可。
關於閉包
閉包指的是在一個lambda表達式的方法體中訪問了不屬於這個方法體的外部變量。在C#4.0以及早期版本的編譯器中,對於下面個例子(例子來源)的執行會產生和一般想法不太一樣的結果:
var values = new List<int>(){ 10, 20, 30}; var funcs = new List<Func<int>>(); foreach (var val in values){ funcs.Add(() => val); } foreach (var f in funcs){ Console.WriteLine((f())); }
乍一看來這段代碼會依次返回10,20,30。但在C#4.0及之前(編譯器隨VS版本而變,可以用VS2012之前的版本測試)的編譯器上測試執行返回3個30。如果VS安裝有Resharper,會得到復制一份變量到本地的提示。
這是因為foreach中這個循環變量如果換成for的形式如下:
int val; for(var i=0;i<3;i++) { val = values[i]; ... }
所以lambda表達式捕獲到的是一個相對於循環作用域的外部變量,最終捕獲到的是循環的最終值30。要想讓結果正確需要把foreach每次的變量復制到本地一份:
var values = new List<int>(){ 10, 20, 30}; var funcs = new List<Func<int>>(); foreach (var val in values){ int valLocal = val; funcs.Add(() => valLocal); } foreach (var f in funcs){ Console.WriteLine((f())); }
這樣輸出結果就是符合一般思維的10,20,30了。
在C#5.0及以后的編譯器中,遇到這種情況會自動復制一份本地實例到循環體中,從而保證結果符合大眾思維。
關於Func<>與Action<> (.NET Framework 3.5)
在早期版本的.NET定義委托要使用delegate關鍵字這樣進行:
delegate int IamAdd(int left, int right);定義這個委托的實例需要這樣:
IamAdd addMethod = new IamAdd((l, r) => l + r);如果使用Func<>,則代碼可以簡化為:
Func<int,int,int> addMethod = (l, r) => l + r;顏值倍增吧。Func有多種重載,.NET Framework3.5中參數最多的重載可以接收最多4個參數,在.NET Framework4以后Func重載數量暴增,最多的重載可以接收16個參數。對於沒有返回值的委托可以使用Action系列重載,和Func使用幾乎一模一樣。
C++11 Lambda表達式
在C#之后傳統的面向對象語言也都紛紛加入lambda表達式,主要是C++和Java,作為一個微軟狗,我認為C# lambda語法最漂亮,C++11的也不錯,Java的和C++11差不多,不知道誰模仿的誰。論語法來說還是C++11的最復雜,這和C++本身有關,又是引用又是值又是指針的。類似C#中的lambda表達式主要用於接收委托類型的地方,C++中的lambda表達式主要用於接收函數指針的地方,可能是模板庫中的方法也可能是自定義的方法。還是先來看一下C++11中lambda表達式的各種語法,然后在來舉一個實際中應用的例子。
C++11中lambda表達式的一般語法如下:
[捕捉列表](參數) mutable ->返回類型 {方法體}
逐一來分析C++11 lambda表達式的組成部分:
[捕捉列表],這里的[]起到了告訴編譯器下面部分是一個lambda表達式的效果。捕捉列表的作用在於,C++不像C#那樣默認捕獲所有父作用域的變量,而是需要程序員手動指定捕獲那些變量。這部分可能的情況有如下幾種:
-
[]:不捕獲任何外部變量,當lambda表達式不屬於任何塊作用域時,捕獲列表必須為空。
-
[var]:以傳值方式捕獲變量
-
[=]:以傳值方式捕獲所有變量
-
[&var]:以引用方式捕獲變量
-
[&]:以引用方式捕獲所有變量
-
[this]:以傳值方式捕獲當前的this指針
這些也是可以混合使用的,比如[&, a]表示使用傳值方式捕獲a,使用引用方式捕獲其他所有變量。
(參數),C++11中參數列表必須指明類型,不能省略,這點與C#不同,如果參數是泛型則lambda中參數的類型用auto表示。另外如果不存在參數,則()可以省略。(如果存在mutable關鍵字,則即使參數列表為空也必須加上括號)
mutable關鍵字,默認C++11的lambda表達式為const函數,即方法體不能修改外部變量,可以通過添加mutable關鍵字將lambda轉為非const函數。
->返回類型,在C++無法推斷返回值類型的情況下,需要使用這個語法手動指定,否則包括箭頭在內的返回類型可以直接省略,而使用自動推斷。
方法體,C++中方法體必須放在{}中,即使只有簡單的一行代碼,且如果lambda有返回值return也不能省略。
說完C++11 Lambda表達式的語法,再來說說其應用。C++中應用Lambda表達式最多的地方還是標准庫中以前接收函數對象的地方,尤其和容器相關的一些算法,下面一個小栗子足以說明一切:
std::vector<int> c{ 1,2,3,4,5,6 }; std::remove_if(c.begin(), c.end(), [](int n) { return n % 2 == 1; });
在C#中,如果要引用Lambda表達式一般都使用Action或Function<T>。同樣在C++引用Lambda表達式可以使用std::function,上面的Lambda表達式可以這樣引用:
std::function<bool (int)> func = [](int n) { return n % 2 == 1; };
模板中,第一個類型表示返回值類型,參數的類型被放在括號中。
C++中Lambda的工作原理很簡單。在內部編譯器將lambda表達式編譯為一個匿名對象,在其中有一個重載的函數調用運算符,其方法體即lambda表達式的方法體。
語言集成查詢 - LINQ
自從C#3.0、.NET Framework3.5提供LINQ支持以后,LINQ已經成了.NET Framework中相當重要的一部分。當然這個LINQ應該不限於下面這種標准的LINQ語法:
int[] list = { 0, 1, 2, 3, 4, 5, 6 }; var numQuery = from num in list where (num % 2) == 0, select num;
還應包括以LINQ思想Fluent API方式的擴展方法的實現:
int[] list = { 0, 1, 2, 3, 4, 5, 6 }; var numQuery = list.Where(i=>i%2==0).Select(i=>i);
之前看過一篇Java社區討論該不該有LINQ的問題,好多人說Java 8中一種名為"Streams"的新語法比LINQ看起來好很多,其實那就是.NET Fluent API的克隆版,而出現卻比.NET的實現完了n年,某些Java程序員還是很有阿Q精神,其實Java及其框架比C#落后好多這是不爭的事實。繼續正題...
LINQ的在.NET中用途太多了,.NET Framework內建對集合類型的LINQ to Object的支持,對XML支持的LINQ To XML,對數據庫支持的LINQ to SQL。另外實體類框架的查詢也是基於LINQ實現的,通過編寫Provider你也可以實現自己的LINQ to xxx。
園子中介紹LINQ的文章的太多了,這一小節就簡單介紹下LINQ的原理,並通過一個例子進行分析。至於如何實現自定義的Provider那樣復雜的話題,請查找相關“專業”文章。
用XMind畫了一個大體的流程圖,電腦上實在沒有其他方便的流程圖工具。
圖1
通過圖可以看到除了LINQ to Object,其他LINQ to XXX都是被做為表達式樹(ExpressionTree,下一小節會看到)在相應的QueryProvider上被“編譯”,這個“編譯”就是QueryProvider上的CreateQuery進行的工作。IQueryProvider接口定義了CreateQuery和Execute兩個方法(算上泛型版本其實是4個)。我們自己實現LINQ to XXX時,最主要的就是實現IQueryProvider接口並在CreateQuery方法中將表達式樹轉為平台特定的查詢,這個過程可能設計表達式樹的遍歷等,下一小節會做說明。在CreateQuery方法過后,平台相關查詢就准備好了 ,但直到GetEnumerator方法被調用才會被實際執行。很多操作,如foreach或ToList都會讓GetEnumerator被調用。實際執行平台相關查詢實在Execute方法中發生的。
可以看到實現一個最基本的LINQ to XXX框架只需要實現IQueryable<T>和IQueryProvider接口兩個方法就可以了。像是EF那種復雜的框架最底層也是通過這兩個接口來完成,只是上層添加了許許多多其他裝置。
本小節最后來一個小小的栗子吧,下面是一段EF中進行查詢的代碼:
var productQuery = from product in context.Set<Product>() where product.Type == ProductType.Book select product.Name; var products = productQuery.ToList();
結合上面的原理分析看一下這段代碼,context.Set方法返回DbSet類對象,DbSet的父類DbQuery就是EF中實現IQueryable接口的類型。代碼中的productQuery可以被看作是一個表達式樹,當productQuery對象生成的時候,由EF實現的QueryProvider生成的T-SQL也就准備好了。當ToList方法被調用時,上面准備的T-SQL被發送到數據庫執行並獲得結果。
相信通過這一小節的介紹,大家應該對LINQ及其原理有個大概的介紹。這里強烈推薦李會軍老師的一篇文章,仔細讀過你就可以更好的理解本小節的內容,而且對實現自己的LINQ to XXX也能有更深入的了解。
下一小節談談上面反復提到的表達式樹。
C# 表達式樹
表達式樹,顧名思義就是以樹的形式來表示表達式。到底表達式樹是什么樣的呢?上一小節提到了LINQ中大量使用表達式樹,我們去就那里面找找表達式樹的痕跡。看一段簡單的LINQ toSQL代碼:
DataContext ctx = new DataContext("...connectionString..."); Table<Product> products = ctx.GetTable<Product>(); var productQuery = products.Where(p => p.Name == "Book");
看看其中定義在Queryable.cs文件中的Where方法
IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
有一個Expression<T>類型的參數,這就是我們要找的表達式樹。
注意,在LINQ to Object或LINQ to XML的Where方法中是看不到Expression<T>類型的,LINQ部分講過,這二者都是直接實現的查詢方法沒有經過QueryProvider。它們的Where方法定義在Enumerable.cs中,形如:
IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)可以看到這個方法接收的參數就是一個普通的Func<T,T>委托對象,它們可以在.NET平台直接執行。
看到這問題來了,我們同樣的lambda表達式既可以傳遞給表達式樹,又可以傳遞給委托。那么表達式樹和委托有什么不一樣呢。其實它們區別還是很大的,lambda表達式本身就是委托類型,可以被看作一個委托的對象,它是一段可以直接被.NET編譯運行的代碼。而lambda到表達式樹經歷了由lambda變成一個LambdaExpression對象的過程。為了能更直觀的看到表達式樹的樣子,把之前的代碼稍作調整:
Expression<Func<Product, bool>> exp = p => p.Name == "Book"; var productQuery = products.Where(exp);
來看一下exp這個表達式樹對象在VS監視中的樣子:
圖2
如圖,Parameters表示僅有一個參數Product類型的p,Body是Lambda的方法體,Type就是表達式樹的類型Func<Product,bool>。最重要的就是這個NodeType,其值Lambda表示這個表達式是一個LambdaExpression。
通過上面的分析可以看到上面代碼能成立最重要的一步就是編譯器可以把Lambda轉為LambdaExpression。對於像是上文這樣一些簡單的Lambda,.NET可以分析其組成並轉為LambdaExpression,對於一些復雜的表達式,我們可能需要手動構造表達式樹。
Expression抽象類包含了Add,Equal,Convert等數十中方法來表示表達式中的計算,通過這些方法的組合可以表示幾乎所有的表達式。除了這些方法Expression中的Lambda方法用於生成LambdaExpression,這樣手動構造的表達式樹就可以用於接收表達式樹的場景中。說了這么多,來看一下怎么手動構造上面提到的表達式:
ParameterExpression paraProduct = Expression.Parameter(typeof(Product), "p"); MemberExpression productName = Expression.Property(paraProduct, "Name"); ConstantExpression conRight = Expression.Constant("Book", typeof(string)); BinaryExpression binaryBody = Expression.Equal(productName, conRight); Expression<Func<Product, bool>> exp = Expression.Lambda<Func<Product, bool>>(binaryBody, paraProduct);
看起來很簡單吧。Expression還提供了Compile方法把一個表達式樹轉為Lambda表達式:
Func<Product, bool> lambda = exp.Compile();
說了這么多,表達式樹到底有什么用呢。博主認為表達式樹一個很大的作用就是把之前需要用字符串的地方換成了表達式,這種強類型可以在編譯時被檢查,有更好的穩定性。比如MVVM Light中那個經典的Set()方法:
bool Set<T>(Expression<Func<T>> propertyExpression, ref T field, T newValue)
這樣可以通過表達式的方式設置更新的屬性,這樣比之前用propertyName那種字符串設置屬性的方式更不容易出錯。
當然表達式樹還有很多用途,在.NET2.0時代我們獲取一個對象的某個屬性(在屬性名為一個字符串的情況下),一般都是通過反射來完成。現在有了表達式樹則可以使用表達式樹來完成同樣的工作。據測試速度要比反射快很多。這方面的文章網上有太多這里就不再多寫了。同時像是老趙等大牛當年還討論過表達式樹的性能問題,如果需要大量應用表達式樹這些都需要去仔細研究。這里就提下綱,對此不了解的園友可以按這個方向去查找相關文章學習。
與LINQ一樣,這個在C++中也沒有等價功能就不寫了。
C# 其它細微變化
自動屬性
C#的自動屬性就是提供了對於屬性傳統寫法一種更簡潔的寫法,比如下面是傳統寫法:
private int _age; public int Age { get { return _age; } set { _age = value; } }
如果我們無需使用_age,則可簡寫為:
public int Age {get;set;}
對於只讀屬性也可以:
public int Age {get;}
分部方法
這個特性還真沒發現有什么用,相對於分部類來說幾乎沒有應用場景。以一個例子簡單說明:
public partial class Sample { partial void SamplePartialMethod(string s); } public partial class Sample { partial void SamplePartialMethod(String s) { Console.WriteLine("Method Invoked with param:",s); } }
分部方法並不能將實現分開放在兩部分(顯而易見,那樣沒法保證執行順序),而是一部分提供一個類似聲明的作用,而另一部分提供真正的實現。
值得注意的是,分部方法默認為private方法且必須返回void。
這兩個語法糖也沒見C++有等價的實現。
預告
第一篇就到此,下一篇將以C#4.0的新特性為軸介紹C#和C++的一些變化。