C#複習筆記(4)--C#3:革新寫代碼的方式(查詢運算式和LINQ to object(上))

來源:互聯網
上載者:User

標籤:sel   post   直接   複習   寫代碼   含義   ted   show   active   

查詢運算式和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 baldMajor-Subtitles only work in WelshMajor-Play button points the wrong wayMinor-Network is saturated when playing WAV fileTrivial-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(上))

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.