C#與C++的發展歷程第一 - 由C#3.0起

俗話說學以至用,本系列的出發點就在於總結C#和C++的一些新特性,並給出實例說明這些新特性的使用場景。前幾篇文章將以C#的新特性爲綱領,並同時介紹C++中類似的功能的新特性,最後一篇文章將總結以前幾篇沒有介紹到的C++11的新特性。html

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再接再礪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# 對象初始化器與集合初始化器

在對象初始化器出現以前,咱們實例化一個對象並賦值的過程代碼看起來是很冗餘的。好比有這樣一個類:異步

1
2
3
4
5
6
class  Plant
{
     string  Name{ get ; set ;}
     string  Category{ get ; set ;}
     int  ImageId{ get ; set ;}
}

實例化並賦值的代碼以下:

1
2
3
4
Plant peony =  new  Plant();
Peony.Name =  "牡丹" ;
Peony.Category=  "芍藥科" ;
Peony.ImageId=6;

若是咱們須要屢次實例化並賦值,爲了節省賦值代碼,能夠提供一個構造函數:

1
2
3
4
5
6
Plant( string  Name, string  Category, int  ImageId)
{
     Name = name;
     Category=category;
     ImageId= imageid;
}

這樣就能夠直接調用構造函數來實例化一個對象並賦值,代碼至關簡潔:

1
Plant peony =  new  Plant( "牡丹" , "芍藥科" ,6);

若是咱們只須要給其中2個屬性賦值,或者類中又增長新的屬性,原來的構造函數可能不能再知足要求,咱們須要提供新的構造函數重載。

如今有了對象初始化器,咱們可使用更簡單的語法來實例化對象並賦值:

1
2
3
4
5
6
Plant peony =  new  Plant
{
     Name =  "牡丹" ,
     Category= "芍藥科" ,
     ImageId= 6
}

咱們能夠根據需求隨意增長或減小對屬性的賦值。

接着來看看集合初始化器,習慣了對象初始化的語法,集合初始化器是水到渠成的:

1
2
3
4
5
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>類的對象也能夠用相似的方式實例化:

1
2
3
4
5
6
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#的對象初始化器是孿生兄弟,就是一對花括號 &ndash; {}。咱們由基本類型的初始化提及。

在C++11以前,咱們初始化一個int通常寫出這樣:

1
int  i(3);

1
int  i = 3;

參見本小節末:初始化和賦值的區別

使用新的初始化語法能夠寫爲:

1
int  i{3};

一樣char類型對象新的初始化方式:

1
char  c{ 'x' };

使用賦值的方式下,下面代碼是能夠工做的:

1
int  f=5.3;

賦值完成後f的值爲5,編譯器進行了窄轉換,而使用新的初始化方式,窄轉換就不會發生,即下面的代碼沒法經過編譯:

1
int  f{5.3}; //注意,類型不匹配,沒法經過編譯

接着看一下類類型的例子:

咱們使用與C#部分類型的類:

1
2
3
4
5
6
7
8
9
10
11
class  Plant
{
   public :
     Plant();
     virtual  ~Plant();
     string m_Name;
     string m_Category;
     unsigned  int  m_ImageId;
   protected :
   private :
};

不一樣於C#使用{}初始化類成員時須要顯式指定類成員名稱,C++類經過定義構造函數來獲知初始化列表中參數的順序。咱們能夠這樣實現一個Plant類的構造函數,其中冒號開始的語法被稱爲"構造函數初始化列表":

1
2
3
4
Plant::Plant(string _name,string _category,unsigned  int  _imageId)
             :m_Name(_name),m_Category(_category),m_ImageId(_imageId)
{
}

別忘了在頭文件中給新的構造函數重載加個聲明,而後就能夠這樣實例化一個Plant對象了:

1
Plant plant{  "牡丹" "芍藥科" , 6};

上面的例子都是在棧上分配的對象,對於堆上分配的對象,也可使用new關鍵字加上新的初始化方式,如對於前面的Plant類,可使用這種方式在堆上實例化一個新的對象:

1
Plant *plant =  new  Plant{  "牡丹" "芍藥科" , 6};

 

對於struct,不須要實現重載構造就可使用統一的初始化語法:

1
2
3
4
5
6
struct  StPlant
{
     string m_Name;
     string m_Category;
     unsigned  int  m_ImageId;
};

能夠直接這樣實例化一個StPlant對象:

1
StPlant stplant{ "牡丹" "芍藥科" , 6};

在C++中聲明,定義,初始化和賦值有着概念上的大不一樣,這對於用慣C#這樣不太區分這種概念的語言的同窗可能感受很不理解。下面依次介紹下這幾個概念:

聲明,例如:

1
extern  int  i;

在類型名前添加一個extern關鍵字表示聲明一個變量,這個變量在其餘連接的文件中被定義。C++中一個變量能夠被聲明不少次但只能被定義一次。

定義:

1
int  i;

定義是最多見的,注意,定義的同時也表示聲明瞭這個變量。

初始化,初始化的方式有兩種:

1
2
int  i(5);
int  i = 5;

前者是直接初始化,後者是拷貝初始化。這二者的不一樣是前者是尋找合適的拷貝/移動構造函數,後者是使用拷貝/移動賦值運算符。C++11後明確引入了右值及移動語意,初始化的性能大大提升。

賦值:

1
2
int  i;
i=5;

這樣把定義與賦值分開,則賦值的過程必定是調用拷貝/移動賦值運算符,而不是經過構造函數來完成。

最後看看下面這種寫法:

1
extern  int  i = 5;

這樣extern會被忽略,這是一個定義(含聲明)及拷貝初始化變量的語句,且這個變量不能被再次定義。

C++11 初始化列表

標準庫中的容器也可使用統一的初始化方式進行填充:

1
std::vector< int > vec = {0, 1, 2, 3, 4};

更復雜一點的栗子:

1
2
3
4
5
vector<Plant> plants = {
     "牡丹" "芍藥科" , 6},
     "牡丹" "芍藥科" , 6},
     "牡丹" "芍藥科" , 6}
};

一樣std::map系列容器也可使用相似的方式初始化:

1
2
3
4
5
map< int , Plant> plantsDic = {
     {1, {  "牡丹" "芍藥科" , 6}},
     {2, {  "牡丹" "芍藥科" , 6}},
     {3, {  "牡丹" "芍藥科" , 6}}
};

 

C++11對初始化列表支持的背後,一個其關鍵做用的角色就是新版標準庫新增的std::initializer<T>模板類。編譯器能夠將{list}語法編譯爲std::initializer<T>類的對象。新版庫中的容器也都添加了接收std::initializer<T>類型參數的構造函數重載,因此上面示例的幾種寫法均可以被支持。vector中增長的構造函數形如:

1
template  < typename  T> vector::vector(std::initializer_list<T> initList);

咱們也能夠在本身的函數實現中使用std::initializer<T>做爲參數,以下代碼:

注意:使用std::initializer<T>須要#include <initializer_list>

 

1
2
3
4
5
6
7
8
9
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}列表。

1
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對象的代碼就像:

1
Tuple< string , string , int > plant = Tuple.Create( "蓮" , "蓮科" ,1);

使用var代碼就能夠簡化爲:

1
var  plant = Tuple.Create( "蓮" , "蓮科" ,1);

在foreach循環中也經常是var的用武之地

1
2
foreach ( var  kvp  in  dictionaryObj)
{}

若是沒有var,咱們就要手寫KeyValuePair<K,V>類型的名稱,若是遍歷的集合類是一個不常見的類型,諸如Enumerable.ToLookup()和Enumerable.GroupBy()方法返回的值,可能都記不清其中每一項的具體類型。使用var就能夠輕鬆表示這一切。

var和後面要介紹的Linq也是結合最緊密的,通常Linq返回的都是一個類型很是複雜的對象。使用var能減小很大的編碼工做量,使代碼保持整潔。

 

匿名類

若是咱們將前文介紹的對象初始化器語法中的new關鍵字類型去掉,這樣就獲得了匿名類,如:

1
2
3
4
5
6
var  peony =  new
{
     Name =  "牡丹" ,
     Category= "芍藥科" ,
     ImageId= 6
}

匿名類中全部屬性都是隻讀的,且其類型都是自動推導得來不能手動指定。當兩個匿名類型具備相同的屬性,則它們被認爲是同一個匿名類型

如這個對象:

1
2
3
4
5
6
var  peach =  new
{
     Name =  "桃花" ,
     Category =  "薔薇科" ,
     ImageId = 7,
};

判斷類型的話,它們是相同的:

1
var  sametype = peony.GetType() == peach.GetType();

匿名類型的屬性能夠直接用另外一個對象的屬性來初始化:

1
2
3
4
5
6
var  football =  new
{
     Name =  "足球" ,
     Size =  "Big" ,
     peach.ImageId,
};

這樣football中就會有一個名爲ImageId的屬性,且值爲7。固然也能夠自定義名稱,若是屬性名相同省略就好。這種用法在LINQ的Select擴展方法中接收的lambda表達式建立新的匿名對象時經常會見到。

 

隱式類型數組

經過隱式類型數組這個特性,聲明並初始化數組時也不用顯式指定數組類型了。編譯器會自動推導數組的類型,如:

一維數組

1
2
var  a =  new [] { 1, 10, 100, 1000 };  // int[]
var  b =  new [] {  "hello" null "。world"  };  // string[]

交錯數組

1
2
3
4
5
var  d =  new []
{
     new []{ "Luca" "Mads" "Luke" "Dinesh" },
     new []{ "Karen" "Suma" "Frances" }
};

隱式類型數組也能夠包含匿名類對象,固然全部的匿名類對象要符合同一個匿名類的定義。

參考下面這段示例代碼:

 

1
2
3
4
5
6
7
8
9
10
11
var  plants =  new [] 
  new 
      Name =  "蓮"
      Categories=  new [] {  "山龍眼目" "蓮科" , "蓮屬" 
  }, 
  new 
      Name =  " 柳"
      PhoneNumbers =  new [] {  "金虎尾目" , "楊柳科" , "柳屬" 
 
};

 

C++11 類型推導

在C++中一樣因爲模板類型的大量使用致使某些類型的對象的類型不容易記憶及書寫。C++11提供了auto關鍵字來解決這個問題。auto關鍵字的用法與C#中的var極爲類似,即在須要指定具體類型的地方代之以auto關鍵字,如方法返回值前,以範圍爲基礎的for循環中。

如:

1
2
3
string s( "some lower case words" );
for ( auto  it=s.begin(); it!=s.end && ! isspace (*it); ++it);
*it= toupper (*it);  //轉換爲大些字母

在C++11中還有一個更爲強大的定義類型的操做符 - decltype。咱們直接看一個例子,再來講明這個關鍵字的用法:

1
2
string s( "some words" );
decltype (s.size()) index=0;

代碼中s.size()返回值的類型爲string::size_type,decltype使用這個類型來定義index變量,代碼中第二句至關於:

1
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()方法完成的功能。通常的寫法以下:

1
2
3
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>來實現這個功能:

1
2
3
4
5
6
7
public  static  class  EnumerableExt
{
     public  static  string  StrJoin<T>( this  IEnumerable<T> enumerable,  string  spliter)
     {
         return  string .Join(spliter, enumerable);
     }
}

能夠看到擴展方法須要定義在靜態類中,且擴展方法自身也須要是靜態方法。擴展方法所在的類的名字不重要,相對而言這個類所在的命名空間的名字更重要,由於是經過引用的命名空間讓編譯器知道咱們擴展方法來自於哪裏。擴展方法最重要的部分爲第一個參數,這個參數前面有一個this,表示咱們要擴展這個參數的類型,擴展方法主要執行在這個參數對象上。除此以外實現擴展方法和實現通常方法相同。使用這個擴展方法重寫以前的代碼後:

1
2
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表達式出現以前,只能經過委託表示一個函數,經過委託的實例或匿名函數來表示一個&ldquo;函數對象&rdquo;。有了lambda表達式,C#2.0中出現的匿名函數就能夠退役了。lambda表達式能夠徹底取代匿名函數實現的功能。並且.NET Framework新增的Action及Func<T>系列委託類型也能夠減小咱們自定義委託類型的必要。

C#的lambda表達式的語法歸納以下:

參數部分 => 方法體

對於參數部分,若是有2個或2個以上的參數須要用小括號括起來,lambda表達式的參數部分參數無需指定類型,編譯器會自動進行類型推導。固然也能夠明確指定參數類型:

1
( int  x) => x+1;

對於方法體部分若是隻有一條語句則無需加{},且對於有返回值的方法體也能夠省略return關鍵字。若是是超過一條語句則須要{}且對於有返回值的狀況不能省略return,如:

1
x => { x=x+1;  return  Math.Pow(x,2);}

C#中lambda表達式通常用於各類和委託類型相關的場景,好比一個方法接收委託類型參數或返回一個委託類型對象。在實現Fluent API樣式的LINQ語法的那些擴展方法中不少都是接收委託類型的參數,如:

1
2
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以及早期版本的編譯器中,對於下面個例子(例子來源)的執行會產生和通常想法不太同樣的結果:

1
2
3
4
5
6
7
8
var  values =  new  List< int >(){ 10, 20, 30};
var  funcs =  new  List<Func< int >>();
foreach  ( var  val  in  values){
     funcs.Add(() => val);
     }
foreach  ( var  in  funcs){
     Console.WriteLine((f()));
}

乍一看來這段代碼會依次返回10,20,30。但在C#4.0及以前(編譯器隨VS版本而變,能夠用VS2012以前的版本測試)的編譯器上測試執行返回3個30。若是VS安裝有Resharper,會獲得複製一份變量到本地的提示。

這是由於foreach中這個循環變量若是換成for的形式以下:

1
2
3
4
5
6
int  val;
for ( var  i=0;i<3;i++)
{
     val = values[i];
     ...
}

因此lambda表達式捕獲到的是一個相對於循環做用域的外部變量,最終捕獲到的是循環的最終值30。要想讓結果正確須要把foreach每次的變量複製到本地一份:

1
2
3
4
5
6
7
8
9
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  in  funcs){
     Console.WriteLine((f()));
}

這樣輸出結果就是符合通常思惟的10,20,30了。

在C#5.0及之後的編譯器中,遇到這種狀況會自動複製一份本地實例到循環體中,從而保證結果符合大衆思惟。

關於Func<>與Action<> (.NET Framework 3.5)

在早期版本的.NET定義委託要使用delegate關鍵字這樣進行:

1
delegate  int  IamAdd( int  left,  int  right);

定義這個委託的實例須要這樣:

1
IamAdd addMethod =  new  IamAdd((l, r) => l + r);

若是使用Func<>,則代碼能夠簡化爲:

1
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表達式最多的地方仍是標準庫中之前接收函數對象的地方,尤爲和容器相關的一些算法,下面一個小栗子足以說明一切:

1
2
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表達式能夠這樣引用:

1
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語法:

1
2
3
4
5
6
int [] list = { 0, 1, 2, 3, 4, 5, 6 };
 
var  numQuery =
     from  num  in  list
     where  (num % 2) == 0,
     select  num;

還應包括以LINQ思想Fluent API方式的擴展方法的實現:

1
2
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中進行查詢的代碼:

1
2
3
4
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代碼:

1
2
3
DataContext ctx =  new  DataContext( "...connectionString..." );
Table<Product> products = ctx.GetTable<Product>();
var  productQuery = products.Where(p => p.Name ==  "Book" );

看看其中定義在Queryable.cs文件中的Where方法

1
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中,形如:

1
IEnumerable<TSource> Where<TSource>( this  IEnumerable<TSource> source, Func<TSource,  bool > predicate)

能夠看到這個方法接收的參數就是一個普通的Func<T,T>委託對象,它們能夠在.NET平臺直接執行。

看到這問題來了,咱們一樣的lambda表達式既能夠傳遞給表達式樹,又能夠傳遞給委託。那麼表達式樹和委託有什麼不同呢。其實它們區別仍是很大的,lambda表達式自己就是委託類型,能夠被看做一個委託的對象,它是一段能夠直接被.NET編譯運行的代碼。而lambda到表達式樹經歷了由lambda變成一個LambdaExpression對象的過程。爲了能更直觀的看到表達式樹的樣子,把以前的代碼稍做調整:

1
2
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,這樣手動構造的表達式樹就能夠用於接收表達式樹的場景中。說了這麼多,來看一下怎麼手動構造上面提到的表達式:

1
2
3
4
5
6
7
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表達式:

1
Func<Product,  bool > lambda = exp.Compile();

說了這麼多,表達式樹到底有什麼用呢。博主認爲表達式樹一個很大的做用就是把以前須要用字符串的地方換成了表達式,這種強類型能夠在編譯時被檢查,有更好的穩定性。好比MVVM Light中那個經典的Set()方法:

1
bool  Set<T>(Expression<Func<T>> propertyExpression,  ref  T field, T newValue)

這樣能夠經過表達式的方式設置更新的屬性,這樣比以前用propertyName那種字符串設置屬性的方式更不容易出錯。

固然表達式樹還有不少用途,在.NET2.0時代咱們獲取一個對象的某個屬性(在屬性名爲一個字符串的狀況下),通常都是經過反射來完成。如今有了表達式樹則可使用表達式樹來完成一樣的工做。據測試速度要比反射快不少。這方面的文章網上有太多這裏就再也不多寫了。同時像是老趙等大牛當年還討論過表達式樹的性能問題,若是須要大量應用表達式樹這些都須要去仔細研究。這裏就提下綱,對此不瞭解的園友能夠按這個方向去查找相關文章學習。

 

與LINQ同樣,這個在C++中也沒有等價功能就不寫了。

 

C# 其它細微變化

自動屬性

C#的自動屬性就是提供了對於屬性傳統寫法一種更簡潔的寫法,好比下面是傳統寫法:

1
2
3
4
5
6
private  int  _age;
public  int  Age
{
     get  return  _age; }
     set  { _age = value; }
}

若是咱們無需使用_age,則可簡寫爲:

1
public  int  Age { get ; set ;}

對於只讀屬性也能夠:

1
public  int  Age { get ;}

 

分部方法

這個特性還真沒發現有什麼用,相對於分部類來講幾乎沒有應用場景。以一個例子簡單說明:

1
2
3
4
5
6
7
8
9
10
11
12
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++的一些變化。

相關文章
相關標籤/搜索