今天,我們通過一個簡單的示例代碼的演進過程,來學習LINQ必備條件:隱式類型局部變量;對象集合初始化器;委托;匿名函數;lambda表達式;擴展方法;匿名類型。廢話不多說,我們直接進入主題。
一、實現要求
1、獲取全部女生;
2、對滿足要求的結果按年齡排序;
3、獲取結果的前兩名;
4、對獲取結果計算平均年齡;
5、輸出結果信息,包含姓名、性別、年齡;
說明:學生類為Student(包含學生完整信息),輸出結果類為:StudentInfo(包含我們關心的信息,后面將演示它是如何消失的)。在此我們不討論示例的實用性,使用它,僅是方便引出我們今天的學習內容。
二、代碼演進
1、傳統方法
現在我們實現第一個要求,找出全部女生。我們平常實現的邏輯大致是:循環學生對象集合,在循環體內逐一判斷每一個學生對象的性別是否為要求的性別,如果是則放進結果集合。循環結束后輸出結果集合中學生信息對象中的數據(這里我們使用ObjectDumper類來輸出信息,它是微軟提供的LINQ示例中的一個類)代碼出下:
1 /// <summary> 2 /// 獲取女生信息並輸出 3 /// </summary> 4 public static void GetSutdents() { 5 //由於Student可能會比較多的字段,而我們只輸出關心的內容, 6 //因此使用StudentInfo類來存在我們關心的信息 7 List<StudentInfo> studentents = new List<StudentInfo>(); 8 foreach (Student student in CreateStudents()) { 9 if (student.Sex == SexType.Woman) { 10 StudentInfo info = new StudentInfo(); 11 info.Age = student.Age; 12 info.Sex = student.Sex;
info.Name = student.Name; 13 studentents.Add(info); 14 } 15 } 16 ObjectDumper.Write(studentents); 17 }
2、隱式類型局部變量
上面的方法是我們經常使用的,平常忙於為老板賺錢的我們可能沒有時間去考慮上面的代碼是否可以精簡,項目中到處充斥着類似的邏輯。《重構》告訴我們要盡量消滅重復的代碼,以寫出優美的、可維護的高質量代碼。我們一起來看看上面的代碼,它有兩個地方出現了重復(studentents、info聲名、studentents、info初始化地方)。這里可以精簡嗎?讓我們少敲幾下鍵盤嗎?答案當然可以。C#3.0提供了一個新的、名為var的關鍵詞,允許我們無須顯示給定類型即可定義一個局部變量。它就是隱式類型局部變量【在使用VAR關鍵字聲明變量,編譯器會通過該變量的初始化代碼來推斷其真正的類型】使用隱式類型局部變量重構上面的代碼,如下所示:
1 /// <summary> 2 /// 獲取女生信息並輸出 3 /// </summary> 4 public static void GetSutdents2() { 5 //由於Student可能會比較多的字段,而我們只輸出關心的內容, 6 //因此使用StudentInfo類來存在我們關心的信息 7 var studentents = new List<StudentInfo>(); 8 foreach (var student in CreateStudents()) { 9 if (student.Sex == SexType.Woman) { 10 var info = new StudentInfo(); 11 info.Age = student.Age; 12 info.Sex = student.Sex;
info.Name = student.Name; 13 studentents.Add(info); 14 } 15 } 16 ObjectDumper.Write(studentents); 17 }
上面的代碼聲明變量通過var關鍵字進行,例如: var studentents = new List<StudentInfo>(); 變量studentents的類型是通過后面的初始化表達式new List<StudentInfo>();來推斷出變量studentents的類型為List<StudentInfo>。雖然這項改進沒有為我們省下多少代碼。但如果在整上項目中來看,能為我們節省不少時間,提高效率。需要說明的是:這里不用擔心性能問題,因為他和顯示聲明的寫法其實是一樣的,只是因為編譯器編譯器幫我們做了點事,可以通過查看中間語言來證明,因此不要被使用var影響性能的觀點所誤導,請放心使用。
3、對象初始化器
接下來我們來看一下循環體里對象info的賦值語句,不知道大家有沒有對這樣的語句感到不舒服,對此我是感覺很不舒服的,但賦值又必須進行,有什么方法能改進嗎?增加相應的構造函數看起來是一項不錯的選擇,至少在能幫我們少寫幾句代碼。但真是這樣嗎?如果賦值的字段發生變化,怎么辦?修改構造函數?這不是有違初衷,本來想少寫幾行代碼,結果不但沒有,反而增加了維護的復雜度,因此增加相應的構造函數不是可取的方法。C#3.0引用了對象初始化器【對象初始化器充許我們在單一語句中為對象指定一個或多個字段/屬性的值】,這樣我們就可以以聲明的方式初始化任意類型的對象。在上面的代碼中使用對象初始化器改造后如下所示:
1 /// <summary> 2 /// 獲取女生信息並輸出 3 /// </summary> 4 public static void GetSutdents3() { 5 //由於Student可能會比較多的字段,而我們只輸出關心的內容, 6 //因此使用StudentInfo類來存在我們關心的信息 7 var studentents = new List<StudentInfo>(); 8 foreach (var student in CreateStudents()) { 9 if (student.Sex == SexType.Woman) { 10 studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex,Name=student.Name }); 11 } 12 } 13 ObjectDumper.Write(studentents); 14 }
賦值部份已經由5行變成1行了,這樣的改進是不是越來越讓我得到了實實在在的好處?
4、委托
至此,上面的代碼是不是已經到了無法重構或者非常完善的地步了呢? 如果現在需求發生改變,用戶要求查詢20歲以下的女生。 這時我們去修改if語句的條件判斷?雖然能完成任務,但這種方法不具備可擴展性,因為需求可能又一次發現變化,且更加復雜,此進我們可能需要一單獨的方法來做為條件判斷,可能是更好的選擇。為了更好的通用性,我們不應該把條件寫死在方法內部,而應該通過外面傳進來。C#2.0提供的委托【委托可以認為是一種對象,用來保存指向函數的指針,類似C++中的函數指針】正好能完成這個任務。現在過濾方法應該是這樣的:接受一個Student對象作為參數,返回一個布爾值(代表該對象是否滿足特定條件)。我們可以自定義一個指向這類過濾方法(相同的返回類型、相同的參數個數且類型相同(注意:嚴格來說,這里的表述是不對的,因為C#4.0引入的協變使方法返回的類型可以不相同,逆變使方法的參數類型可以不相同))的委托,但C#2.0提供了能滿足我們需求的內置委托類型(delegate Boolean Predicate<T>(T obj);),通過委托來完成新的需要(查詢20歲以下的女生)的代碼如下:
1 /// <summary> 2 /// 獲取女生信息並輸出(通過委托實現) 3 /// </summary> 4 public static void GetSutdents4(Predicate<Student> match ) { 5 //由於Student可能會比較多的字段,而我們只輸出關心的內容, 6 //因此使用StudentInfo類來存在我們關心的信息 7 var studentents = new List<StudentInfo>(); 8 foreach (var student in CreateStudents()) { 9 if (student.Sex == SexType.Woman) { 10 studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex, Name = student.Name }); 11 } 12 } 13 ObjectDumper.Write(studentents); 14 } 15 16 /// <summary> 17 /// 條件過濾方法 18 /// </summary> 19 /// <param name="student"></param> 20 /// <returns></returns> 21 private static bool Filter(Student student) 22 { 23 return student.Sex == SexType.Woman && student.Age < 20; 24 } 25 26 /// <summary> 27 /// 主程序 28 /// </summary> 29 /// <param name="args"></param> 30 static void Main(string[] args) 31 { 32 //調用通過委托實現過濾的GetSutdents 33 GetSutdents4(Filter); 34 }
5、匿名函數
雖然我們通過委托解決了通用問題,但增加了一個函數。《重構》中提到的一種重構手法--內聯函數(一個函數本體與名稱同樣清楚易懂時,在函數調用點插入函數本體,然后移除該方法),我們剛提取出來,又內聯回去,這不是在做無用功嗎?看來我們得在提取和內聯之間找到平衡點,C#2.0中的匿名函數【無需聲明一個類似Filter的方法,而只需要將這部分邏輯直接傳遞給GetSutdents4方法即可】正是我們要找的這個平衡點。雖然我們沒有聲明方法,但編譯器會為我們生成,匿名函數增強了委托,降低了代碼量。 使用匿名方法調用代碼如下:
1 /// <summary> 2 /// 主程序 3 /// </summary> 4 /// <param name="args"></param> 5 static void Main(string[] args) { 6 //使用匿名方法調用GetSutdents4 7 GetSutdents4(delegate(Student student) { 8 return student.Sex == SexType.Woman && student.Age <= 20; 9 } 10 ); 11 }
6、Lambda表達式
雖然使用匿名方法已經給我們降低了代碼,但可讀性確降低了。為此,C#3.0引入了更為簡潔的Lambda表達式。它直接將函數編程的精彩表達能力引入到了代碼中。它與匿名方法相比提供了如下的一些額外功能(下面4點引用至LINQ IN ACTION):
a、Lambda表達式能夠推導出參數的類型,因此程序中無需顯式聲明;
b、Lambda表達式支持用語句塊或表達式作為方法體,語法上比匿名方法更加靈活(匿名方法的方法體只能用語句塊);
c、在以參數形式傳遞時,Lambda表達式能夠參與到參數類型推斷及對重載方法的選擇中。
d、帶有表達式體的Lambda表達式能夠轉化為表達式樹;
Lambda表達式的寫法如下圖所示,它由三個部分組成1、參數;2、Lambda操作符(=>讀作:goes to (導出));3、表達式或語句塊;
使用Lambda表達式修改后的代碼如下:
1 /// <summary> 2 /// 主程序 3 /// </summary> 4 /// <param name="args"></param> 5 static void Main(string[] args) { 6 //使用Lambda表達式調用GetSutdents4 7 GetSutdents4((student=>student.Sex==SexType.Woman && student.Age<20) ); 8 }
通過引入Lambda表達式后,代碼更加清晰自然了,同時也滿足了簡明、通用的要求。至此,第一個要求就算完成了。接下來我們將完成排序、獲取結果集中前兩名女生及計算其平均年齡的功能。
7、擴展方法
如果我們要對上面獲取的結果集合排序、計算平均值等操作,使用傳統的方法的話,不用說大家也明白要寫多少代碼吧,這里我們就不再去講述傳統方法了,直接進入主題。我們將使用擴展方法來實現上面的要求,同時也展現使用LINQ帶的擴展方法的魔力。擴展方法【用來在類型定義完成后,由於某些原因不能修改源類型的情況下,繼續為基添加新方法】C#中定義擴展方法必須在非泛型的靜態類中定義一個靜態方法,此方法能夠接受任意多個參數,但是第一個參數的類型必須和所擴展的類型一致,且用this關鍵修飾。LINQ為我們帶來了一系列的擴展方法(不管是否用到了LINQ,我們都可以根據實際需要使用他們)。使用擴展方法修改后代碼如下所示:
1 /// <summary> 2 /// 獲取女生信息並輸出 3 /// </summary> 4 public static void GetSutdents5(Predicate<Student> match) { 5 //由於Student可能會比較多的字段,而我們只輸出關心的內容, 6 //因此使用StudentInfo類來存在我們關心的信息 7 var studentents = new List<StudentInfo>(); 8 foreach (var student in CreateStudents()) { 9 if (student.Sex == SexType.Woman) { 10 studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex, Name = student.Name }); 11 } 12 } 13 14 //按年齡排序后獲取前兩個女學生並求年齡的平均值 15 var average = studentents.OrderBy(s => s.Age) 16 .Take(2) 17 .Average(s => s.Age); 18 ObjectDumper.Write(average); 19 }
看到這些擴展方法帶來的魔力了吧,后面幾個要求,被這么簡單的一句鏈式調用【通過“.”對所需方法進行連續調用,就像串起來的鏈一樣】就完成了。其中OrderBy為定義於System.Linq.Enumerable類中的擴展方法。它們的用途,這里就不贅述了。
8、匿名類型
這是開始LINQ之前的最后一個重量級的的C#語言特性了,匿名類型【能像對象初化器一樣構建事先沒有定義的類型,編譯器會幫我定義(又是編譯器,真是一位強大的助手,也正是因為編譯器在后面幫我們做了幕后工作,雖然這些在C#3.0中定義的語言特性編譯后能運行在.NET2.0上,而無需引用那龐大的.NET3.0或.NET3.5,當然上面用到的擴展方法需要System.Runtime.CompilerServices.ExtensionAttribute屬性的支持。但我們可以自行引用入或將System.Core.dll和.NET2.0一起分發)】 上面我們為了返回關心的學生信息而定義一個StudentInfo類,其實有了匿名類型后,像這種簡單的類,可以不用特意去定義,從而節約時間。這能減少系統中的一些無行為的(只是一個數據容器)的雜亂的類。使用匿名類型修改后的代碼如下:
1 /// <summary> 2 /// 獲取女生信息並輸出(使用匿名類型獲取關心的信息) 3 /// </summary> 4 public static void GetSutdents6() { 5 var studentents = new List<object>(); 6 foreach (var student in CreateStudents()) { 7 if (student.Sex == SexType.Woman) { 8 studentents.Add(new { Age = student.Age, Sex = student.Sex, Name = student.Name }); 9 } 10 } 11 ObjectDumper.Write(studentents); 12 }
從上面的代碼,我們可以看到,StudentInfo類型不見了,它已經被匿名類型 new { Age = student.Age, Sex = student.Sex, Name = student.Name } 所代替。
三、結束語
至此,學習LINQ前需要准備的知識已經介紹完成。希望能給你來幫助,或是溫習那曾經熟悉卻又不小心忘記的知識。如有什么不恰當的地方,懇請指正!如果你喜歡或是期待后面的介紹,請點推薦支持。再次謝謝。