俗話說學以緻用,本系列的出發點就在于總結C#和C++的一些新特性,并給出執行個體說明這些新特性的使用場景。前幾篇文章将以C#的新特性為綱領,并同時介紹C++中相似的功能的新特性,最後一篇文章将總結之前幾篇沒有介紹到的C++11的新特性。
C++從11開始被稱為現代C++(Modern C++)語言,開始越來越不像C語言了。就像C#從3.0開始就不再像Java了。這是一種超越,帶來了開發效率的提高。
一種語言的特性一定是與這種語言的類型和運作環境是分不開的,是以文章中說C#的新特性其中也包括新的.NET Framework和CLR(DLR)對C#的支援。
系列文章目錄
1. C#與C++的發展曆程第一 - 由C#3.0起
2. C#與C++的發展曆程第二 - C#4.0再接再厲
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會被忽略,這是一個定義(含聲明)及拷貝初始化變量的語句,且這個變量不能被再次定義。extern int i = 5;
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);
如果使用Func<>,則代碼可以簡化為:IamAdd addMethod = new IamAdd((l, r) => l + r);
顔值倍增吧。Func有多種重載,.NET Framework3.5中參數最多的重載可以接收最多4個參數,在.NET Framework4以後Func重載數量暴增,最多的重載可以接收16個參數。對于沒有傳回值的委托可以使用Action系列重載,和Func使用幾乎一模一樣。Func<int,int,int> addMethod = (l, r) => l + r;
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中,形如:可以看到這個方法接收的參數就是一個普通的Func<T,T>委托對象,它們可以在.NET平台直接執行。IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
看到這問題來了,我們同樣的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++的一些變化。