C#復習筆記(4)--C#3:革新寫代碼的方式(查詢表達式和LINQ to object(上))
查詢表達式和LINQ to object(上)
本章內容:
- 流式處理數據和延遲執行序列
- 標準查詢操作符和查詢表達式轉換
- 範圍變量和透明標識符
- 投影、過濾和排序
- 聯接和分組
- 選擇要使用的語法
LINQ中的概念介紹
序列
你當然應該對序列這個概念感覺很熟悉: 它通過IEnumerable 和 IEnumerable< T> 接口進行封裝,序列就像數據項的傳送帶——你每次只能獲取它們一個, 直到你不再想獲取數據, 或者序列中沒有數據了。
序列與其他的數據集合結構相比最大的區別在於,你通常不知道序列有多少項構成--或者不能訪問任意項,只能是當前這個。列表和數組也能作為序列, 因為 List< T> 實現了IEnumerable< T>—— 不過, 反過來並不總是可行。 比如,你不能擁有一個無限的數組或列表。
序列是LINQ的基礎。一開始查詢表達式總是針對某個序列,隨著操作的進行,可能會轉換序列,也可能和更多的序列鏈接在一起。
來看一個列子:
var adaultNames = from person in People where person.Age > 18 select person.Name;
下面以圖的形式將這個操作拆分成了步驟:
在分解這個步驟之前,先講為什麽序列在LINQ中的地位是如此重要: 這是由於, 它們是數據處理的流模型的基礎, 讓我們能夠只在需要的時候才對數據進行獲取和處理。
上圖中每一個箭頭代表一個序列——描述在左邊, 示例數據在右邊。 每個框都代表查詢表達式的一個步驟。 最初,我們具有整個家庭成員(用Person對象表示)。接著經過過濾後, 序列就只包含成人了(還是用Person對象表示)。 而最終的結果以字符串形式包含這些 成人的名字。每個步驟就是得到一個序列, 在序列上應用操作以生成新的序列。 結果不是字符串"Holly" 和"Jon"—— 而是 IEnumerable<String>, 這樣,在從裏面一個接一個獲取元素的時候, 將首先生成"Holly", 其次得到"Jon"。
再來看一下背後的東西:首先創建的這個表達式,創建的這個表達式只是在內存中生成一個查詢的表現形式,這個表現形式使用委托來表示的。只有在訪問這個結果集(adaultNames)的第一個元素的時候,整個”車輪“才會滾滾向前(把叠代器比喻為車輪)。LINQ的這個 特點稱為延遲執行。 在最終結果的第一個元素被訪問的時候, Select轉換才會為它的第一個元素調用Where轉換。 而Where轉換會訪問列表中的第一個元素, 檢查這個謂詞是否匹配(在這個例子中,是匹配的), 並把這個元素返回給Select。 最後,依次提取出名稱作 為結果返回。當然,相關的各種參數必須執行可控性檢查,如果你要實現自己的LINQ操作符,牢記這一點非常重要。
下圖展示了當用foreach調用結果序列中的每一項時,查詢表達式在運行中的幾個階段
這就是流式的特點,雖然涉及了幾個階段,不過,這種使用流的方式處理數據是很高效和靈活的。特別是,不管有多少數據源,在某個時間點上你只需要知道其中一個就好了。
與流式傳輸相比,還有一種緩沖式的,因為你有的時候必須吧序列中的元素全部加載到內存中來進行計算,比如Reverse操作。他需要提取序列中的所有可用數據,以便把最後一個元素作為第一個元素返回。當然這在效率上會對整個表達式的執行造成很大的性能影響。
不管時流式傳輸還是緩沖式的傳輸,他們都屬於延遲操作,就是只有在枚舉結果集中的第一個元素時才會真正的傳輸數據,與此相對的是立即執行——有一些轉換一經調用就會立即執行。一般來說,返回另一個序列的操作(通常是IEnumerable<T>和IQueryable<T>)會進行延遲操作,返回單一值的運算會立即執行。
LINQ標準查詢操作符
LINQ的標準查詢操作符是一個轉換的集合, 具有明確的含義。標準查詢操作符擁有共同的含義,但在不同的LINQ提供器下,語義也不同,有的LINQ提供器可能在獲取第一個元素的時候就加載了所有的數據,比如web服務。這提示我們在編寫查詢操作時要考慮到使用的是什麽數據源。
C#3支持的某些標準查詢操作符通過查詢表達式內置到語言中。
linq to object的擴展包morelinq可以通過nuget下載。還有Reactive Extensions。
本章的實例數據
要開始實踐本章的內容需要有一個示例數據,這個示例數據在C# in depth的網站上有,不過,你可以通過百度找到這個樣例數據。這裏就不會放出來了,太占篇幅。
簡單的開始:選擇元素
static void Main(string[] args) { // ApplicationChooser.Run(typeof(Program), args); var query = from user in SampleData.AllUsers select user; foreach (User user in query) { Console.WriteLine(user); } Console.ReadKey(); }
註意:SampleData.AllUsers這個是示例數據
查詢表達式就是用粗體標出的那部分。
這個例子沒什麽用,我們可以在foreach中直接用SampleData.AllUsers。————我們會用這個例子來引出兩個概念:①轉譯②範圍變量。
首先看轉譯:編譯器把查詢表達式轉譯為普通的C#代碼, 這是支持C#3查詢表達式的基礎。轉譯的時候他不會檢查錯誤,也不會檢查有效,就是機械的去轉譯。上面的例子被轉譯如下:
var query = SampleData.AllUsers.Select(user => user);
可以看出轉譯的目標是一個方法調用。C#3的編譯器進一步的便宜代碼之前,會先將查詢表達式轉譯成這個樣子。特別的,它不會檢查到底使用Enumerable.Select,還是用List<T>.Select,這會在轉譯後有編譯器進一步的去決定。轉譯只關註後續的編譯器能否正常的編譯轉譯後的代碼——它只負責這個環節。重要之處在於,lmabda能夠被轉換成委托和表達式樹,這個是後續編譯器做的事情的基礎。稍後,在我介紹某些由編譯器調用的方法的簽名時, 記住在LINQ to Objects中只進行一種調用—— 任何時候,參數(大部分) 都是委托類型, 編譯器將用Lambda表達式作為實參, 並盡量查找具有合適簽名的方法。 還必須記住, 在轉譯執行之後, 不管Lambda 表達式中的普通變量(比如方法內部的局部變量) 在哪出現, 它都會以我們在前面的章節看到的方式轉換成捕獲變量。 這只是普通Lambda表達式的行為—— 不過除非你理解哪些變量將被捕獲, 否則很容易被查詢結果弄糊塗。
查詢表達式實現原理
class Dummy<T> { public Dummy<T> Select<T>(Func<T, bool> predicate) { Console.WriteLine("Select called"); return new Dummy<T>(); } } static class Extenssion { public static Dummy<T> Where<T>(this Dummy<T> dummy, Func<T, bool> predicate) { Console.WriteLine("Where called"); return dummy; } }
static void Main(string[] args)
{
var source = new Dummy<string>();
var query = from dummy in source
where dummy.ToString() == "Ignored"
select "Anything";
Console.ReadKey();
}
上面的代碼印證了我們一開始所說的,轉譯就是這麽工作的,我們隨便在某一個類型上面定義一些實例方法和擴展方法然後就可以用查詢表達式來編寫查詢,轉譯的時候根本不在乎你是不是使用了基於IEnumerable的一些擴展方法。所以,他會被轉以為下面的代碼:
var query = source.Where(dummy => dummy.ToString() == "Ignored").Select(dummy => "anything");
註意在查詢表達式select中使用的是”Anything"而不是dummy這是因為select dummy這種特殊的情況會被轉譯後刪除。我的理解是加不加都沒啥用,不影響。
註意Dummy這個類實際上並沒有實現IEnumerable<T>,這說明了轉譯並不依賴具體的類型而是依賴具體的方法名稱和參數,這也是一種鴨子類型,C#的很多地方都是鴨子類型,比如枚舉器能夠枚舉的根本原因是要找到類型中是否包含一個GetEnumerator的方法,還有async和await也是,這個在後面在做說明。
然後再來看另一個概念:範圍變量
還是上面的那個查詢表達式,上下文關鍵字很容易解釋—— 它們明確告知編譯器我們要對數據進行的處理。 同樣,數據源表達式也僅僅是普通的C#表達式—— 在這個例子中是一個屬性,不過它也可以是一個簡單的方法調用或變量。
這裏較難理解的是範圍變量聲明和投影表達式。範圍變量不像其他種類的變量。在某些方面,它根本就不是變量。 它們只能用於查詢表達式中, 實際代表了從一個表達式傳遞給另外一個表達式的上下文信息。 它們表示了特定序列中的一個元素,而且它們被用於編譯器 轉譯中,以便把其他表達式輕易地轉譯為Lambda表達式。
我們已經知道最初的查詢表達式會轉換為如下形式:
SampleData.AllUsers.Select(user => user)
lambda表達式的左邊,就是範圍變量,而右邊,就是select子句的邏輯,轉譯的過程就是這麽簡單。
在更復雜的轉譯過程中,比如SampleData.AllUsers.Select(user => user.Name),也是依賴於C#3更加完善的類型推斷,他把所有的類型參數看作一個整體,可以根據一個類型參數來推斷出另外一個類型參數,而這也是lmabda表達式允許使用隱式類型的原因。一切都歸功於C#3更加強大和完善的類型推斷。(其實在前面的章節中有描述)。
到目前為止,我們都實在一個強類型的集合上面使用查詢操作符,但是,還有一些弱類型的集合,比如ArrayList和object[],這個時候,Cast和OfType操作符就排上用場了。
static void Main(string[] args) { ArrayList list = new ArrayList() { "first", "second", "third" }; IEnumerable<string> strings = list.Cast<string>(); foreach (string item in strings) { Console.WriteLine(item);//依次輸出"first","second","third" } ArrayList anotherList = new ArrayList() { 1,"first",3,"fourth" }; IEnumerable<int> ints = anotherList.OfType<int>(); foreach (int item in ints) { Console.WriteLine(item);//依次輸出1,3 } Console.ReadKey(); }
在將這種弱類型的集合轉換成強類型的集合時,Cast和OfType的機制有所不同,Case會嘗試轉換每一個元素,遇到不支持的類型時,就會報錯,但註意報錯的時機:只有在輸出1之後,才進行報錯,因為Cast和OfType都對序列進行流處理。而OfType會嘗試去轉換每一個元素,跳過那些不合格的元素。
Cast和OfType只允許一致性、拆箱和裝箱轉換。List<int>和List<short>之間的轉換會失敗——Cast會報異常,OfType不會。
而在查詢表達式中,顯示的聲明範圍變量的類型和Cast的執行綁定到了一起:如果在一個弱類型的集合中顯示的聲明範圍變量的類型:
static void Main(string[] args) { ArrayList list = new ArrayList() { "first", "second", "third" }; var query = from string oneString in list select oneString.Substring(0, 3); foreach (string item in query) { Console.WriteLine(item); } Console.ReadKey(); }
這個被轉譯後就會編程這樣
var anotherQuery = list.Cast<string>().Select(li => li.Substring(0, 3));
沒有這個類型轉換(Cast)我們根本就不能調用Select————因為Select是只能用於IEnumerable<T>而不能用於IEnumerable。。
當然,除了在弱類型的集合中使用顯式聲明的範圍類型變量,在強類型中也會這樣使用。比如,List<接口>中你可能想使用顯式類型為”接口實現“聲明的範圍類型,因為你知道這個List中裝的都是”接口實現“而不是”接口“。
接下來闡述一些重要的概念:
- LINQ以數據序列為基礎, 在任何可能的地方都進行流處理。
- 創建一個查詢並不會立即執行它:大部分操作都會延遲執行。
- C#3的查詢表達式包括一個把表達式轉換為普通C#代碼的預處理階段,接著使用類型推斷、重載、Lambda表達式等這些常規的規則來恰當地對轉換後的代碼進行編譯。
- 在查詢表達式中聲明的變量的作用:它們僅僅是範圍變量,通過它們你可以在查詢表達式內部一致地引用數據。
對序列進行過濾和排序
where
這個介紹了很多變的過濾功能的操作符為我們揭開了一些秘密,比如流式傳輸。編譯器把這個子句轉譯為帶有Lambda表達式的Where方法調用,它使用合適的範圍變量作為這個Lambda表達式的參數, 而以過濾表達式作為主體。過濾表達式當作進入數據流的每個元素的 謂詞,只有返回true的元素才能出現在結果序列中。使用多個where子句, 會導致多個鏈接在一起的Where調用——只有滿足所有謂詞的元素才能進入結果序列。
static void Main(string[] args) { User tim = SampleData.Users.TesterTim; var query = from defect in SampleData.AllDefects where defect.Status != Status.Closed where defect.AssignedTo == tim select defect.Summary; foreach (string item in query) { Console.WriteLine(item); } Console.ReadKey(); }
上面這個包含兩個where的查詢表達式會被轉譯成這樣:
var anotherQurty = SampleData.AllDefects .Where(de => de.Status != Status.Closed) .Where(de => de.AssignedTo == tim) .Select(de => de.Summary);
我們當然可以將兩個where合並成一個,這或許能夠提高一些性能,但也要考慮可讀性。
查詢表達式的退格
退格的意思是,如果一個select操作符什麽都不做,只是返回給定序列的相同序列,那麽轉譯後的代碼中就會刪除select的相關的調用:
var myFirstQuery = from def in SampleData.AllDefects select def;
上面這段代碼在轉譯後編譯器會故意加一個select的操作符在後面,不要以為我說粗了,等我全部表述完了,你就明白了:
var mySecondQuery = SampleData.AllDefects.Select(de => de);
在上面增加一個Select和不增加還有有根本的區別的,Select方法表達的是返回一個新的序列的意思,意思是說我們並沒有在原始數據上面進行任何CRUD操作,只是返回一個新的數據源,在這個數據源上面進行操作,是不會對原始數據造成任何影響的。
當有其他操作存在的時候, 就不用為編譯器保留一個“空操作” 的select子句了。 例如,假設我們把上面where下面的那個代碼塊中的查詢表達式改為選取整個缺陷而不僅僅是姓名:
User tim = SampleData.Users.TesterTim; var query = from defect in SampleData.AllDefects where defect.Status != Status.Closed where defect.AssignedTo == tim select defect;
現在我們不需要select的調用,轉譯後的代碼如下:
var anotherQuery = SampleData.AllDefects.Where(defec => defec.Status != Status.Closed) .Where(defec => defec.AssignedTo == tim);
使用orderby子句進行排序
... User tim = SampleData.Users.TesterTim; var query = from defect in SampleData.AllDefects where defect.Status != Status.Closed where defect.AssignedTo == tim orderby defect.Severity descending select defect; ....
如果你下載了本章的樣例代碼,那麽會返回下面的結果:
Showstopper-Webcam makes me look bald Major-Subtitles only work in Welsh Major-Play button points the wrong way Minor-Network is saturated when playing WAV file Trivial-Installation is slow
可以看到已經返回兩個Major,但是這兩個Major如何進行排序呢?我們進一步改造代碼:
.... User tim = SampleData.Users.TesterTim; var query = from defect in SampleData.AllDefects where defect.Status != Status.Closed where defect.AssignedTo == tim orderby defect.Severity descending,defect.LastModified select defect; ....
我們在根據defect.Severity descending進行排序後,又根據defect.LastModified進行類排序。
這個語句被轉譯為下面的代碼:
.... var anotherQeury = SampleData.AllDefects .Where(de => de.Status != Status.Closed) .Where(de => de.AssignedTo == tim) .OrderByDescending(de => de.Severity) .ThenBy(de => de.LastModified); ....
可以看到”orderby defect.Severity descending,defect.LastModified“這句是被翻譯成了OrderBy...ThenBy的形式。同時select被去掉了,原因上面有解。
下面來總結一下orderby子句的原理:
- 它們基本上是上下文關鍵字orderby,後面跟一個或多個排序規則。
- 一個排序規則就是一個表達式(可以使用範圍變量),後面可以緊跟ascending或descending關鍵字, 它的意思顯而易見(默認規則是升序。)
- 對於主排序規則的轉譯 就是調用OrderBy或OrderByDescending,而其他子排序規則通過調用ThenBy或ThenByDescending來進行轉換,正如我們例子中看到的。OrderBy和ThenBy的不同之處非常簡單:OrderBy假設它對排序規則起決定作用,而ThenBy可理解為對之前的一個或多個排序規起 輔助作用。
- 可以使用多個orderby子句,但是只有最後那個才會”勝利“,也就是說前面的那幾個都沒用了。
- 應用排序規則要求所有數據都已經載入(至少對於LINQ to Objecs是這樣的)——例如,你就不能對一個無限序列進行排序。這個原因是顯而易見的,比如,在你看到 所有元素之前,你不知道你看到的某些東西是否出現在結果序列的開頭。
C#復習筆記(4)--C#3:革新寫代碼的方式(查詢表達式和LINQ to object(上))