C#學習筆記(八)—–LINQ查詢的基礎知識(中)

LINQ查詢(中)

  • (接上文)Lambda表達式及Func方法簽名:標準的查詢運算符使用了一個泛型Func委託,Func是System.Linq命名空間中一組通用的泛型委託,它的作用是保證Func中的參數順序和Lambda表達式中的參數順序一致。因此,一個Fuc<TSource,bool>對應的Lambda表達式爲TSource=>bool,也就是接受一個TSource,返回bool。
    類似的,Func<TSource,TResult>所對應的Lambda表達式爲TSource=>TResult。
  • Lambda表達式和元素類型:標準的查詢運算符使用下面這些泛型:
**泛型類型**          **名稱意義**
  TSource            輸入集合的元素類型
  TResult            輸出集合的元素類型(不同於TSource)
  TKey               在排序、分組或者連接操作中所用的鍵

這裏的TSource有輸入集合的元素類型決定。而TResult和TKey則由我們給出的lambda表達式指定。以Select的簽名爲例:

public static IEnumerable<TResult> Select<TSource,TResult>
(this IEnumerable<TSource> source, Func<TSource,TResult> selector)

Func<TSource,TResult>對應的Lambda表達式是TSource=>TResult,這個表達式定義了輸入元素和輸出元素之間的映射關係,實際上TSource和TResult可以是不同的數據類型。更進一步說,Lambda表達式可以指定輸出序列的類型。也就是說Select運算符可以根據Lambda表達式中的定義將輸入類型轉換成輸出類型。下面這個示例中使用Select運算符將string類型的集合元素轉換成int類型的數據來輸出:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<int> query = names.Select (n => n.Length);
foreach (int length in query)
Console.Write (length + "|"); // 3|4|5|4|3|

編譯器通過判斷Lambda表達式中返回值的類型,推斷出TRsult的類型。在這個示例中,推斷TResult爲int型。
Where查詢運算符的內部操作比Select查詢運算符要簡單一些,因爲他只篩選集合,不對集合中的元素進行類型轉換,因此不需要進行類型推斷。它的簽名如下:

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

最後我們看一下Orderby運算符的方法簽名:

// Slightly simplified:
public static IEnumerable<TSource> OrderBy<TSource,TKey>
(this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)

Func<TSource,TKey>將每一個輸入元素關聯到一個排序鍵TKey,TKey的類型也是由Lambda表達式中推測出來的,但他的類型與輸入類型和輸出類型是無關的,三者是獨立的,類型可以相同也可以不同。例如,我們可以選擇對names集合按照名字的長度進行排序(TKey是int),也可以對names集合按字母排序(TKey是string):

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> sortedByLength, sortedAlphabetically;
sortedByLength = names.OrderBy (n => n.Length); // int key
sortedAlphabetically = names.OrderBy (n => n); // string key

這裏寫圖片描述

自然排序

LINQ中集成了對集合的排序功能,這種內置的排序對整個LINQ體系來說有重要的意義。因爲一些查詢操作直接依賴於這種排序,例如:Take、Skip、和Reverse。
Take運算符會輸出集合中前x個元素,這個x以參數的形式指定,例如:

int[] numbers = { 10, 9, 8, 7, 6 };
IEnumerable<int> firstThree = numbers.Take (3); // { 10, 9, 8 }

Skip運算符會跳過集合中的前x個元素,輸出其餘元素:例如:

IEnumerable<int> lastTwo = numbers.Skip (3); // { 7, 6 }

Reverse運算符則會將集合中的所有元素反轉,也就是按照元素當前順序的逆序排列:

IEnumerable<int> reversed = numbers.Reverse(); // { 6, 7, 8, 9, 10 }

Where和Select這兩個查詢運算符在執行時,將集合中元素按照原有的順序進行輸出,事實上,在LINQ中,除非有必要,否則各個查詢運算符都不會告便集合中元素的排序方式。

其他查詢運算符

在LINQ中,並不是所有的查詢運算符都會返回一個集合,一些針對元素的運算符可以從輸入集合衆返回單個元素,如First、Last、ElementAt等查詢運算符,下面是幾個簡單示例:

int[] numbers = { 10, 9, 8, 7, 6 };
int firstNumber = numbers.First(); // 10
int lastNumber = numbers.Last(); // 6
int secondNumber = numbers.ElementAt(1); // 9
int secondLowest = numbers.OrderBy(n=>n).Skip(1).First(); // 7

而聚合運算符則返回一個表示數量的值:

int count = numbers.Count(); // 5;
int min = numbers.Min(); // 6;

下面這些量詞運算符返回bool型的結果:

bool hasTheNumberNine = numbers.Contains (9); // true
bool hasMoreThanZeroElements = numbers.Any(); // true
bool hasAnOddElement = numbers.Any (n => n % 2 != 0); // true

由於這些運算符返回的不是一個集合,所以無法在他們的後面再使用其他查詢運算符,這一點很容易理解,所以這些運算符一般都不現在一個查詢的結尾。
有些查詢運算符同時接受兩個輸入集合,例如Concat運算符會把一個集合衆的元素添加到另一個元素的集合中,另外還有Union運算符,他和Concat運算符的作用是相同的,唯一的區別是Union運算符會將結果集合中相同的元素去掉:

int[] seq1 = { 1, 2, 3 };
int[] seq2 = { 3, 4, 5 };
IEnumerable<int> concat = seq1.Concat (seq2); // { 1, 2, 3, 3, 4, 5 }
IEnumerable<int> union = seq1.Union (seq2); // { 1, 2, 3, 4, 5 }

實際上,連接運算符也屬於這一類,這在後面的章節中會繼續介紹。

查詢表達式

C#中新增了一組專門用於LINQ查詢的語法結構,這種語法結構和C#原有的語法差別很顯著,這種語法結構用到的查詢運算符如from、select、where等和SQL中的關鍵字很類似,但實際上這種語法結構並不是基於SQL設計出來的,而是來源於諸如LISP和Haskell這樣的函數式編程語言。C#借鑑了這些語言中的列表解析方式。
在之前的章節中,我們以查詢表達式流的方式寫過這樣一段代碼,他可以篩選出names集合中的所有含字母a的元素,並把這些元素排序後以大寫形式輸出。下面使用查詢表達式語法來完成相同的操作:

using System;
using System.Collections.Generic;
using System.Linq;
class LinqDemo
{
static void Main()
{
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query =
from n in names
where n.Contains ("a") // Filter elements
orderby n.Length // Sort elements
select n.ToUpper(); // Translate each element (project)
foreach (string name in query) Console.WriteLine (name);
}
}
JAY
MARY
HARRY

查詢表達式一般以from字句開始,最後一select或者group字句結束。from子句的作用是定義一個範圍變量,(本例中是n),這個變量會分別被輸入序列中的每個元素賦值,通過這個變量,可以操作序列中的所有元素,實際上和foreach循環中的臨時變量的作用是相同的。下圖展示了完整的語法:
這裏寫圖片描述
提示:要理解上面查詢表達式中邏輯關係,可以從表達式的最左邊開始把整個表達式看作是一個隊列,例如,在一個表達式中,在from子句之後,可以選擇性的使用orderby、where、let、或者join子句。在所有這些子句之後,我們可以接着使用select或者group來結束整個查詢。也可以不直接結束查詢,而是把上次的查詢結果是做一個輸入,重新使用from、orderby、where、let或者join子句進行第二輪查詢。

  • 編譯器在執行查詢表達式之前會把他編譯成運算符流的形式,這樣更接近它的原始狀態。這個過程非常機械化,並沒沒有使用什麼特別操作,都是最常用的基本操作。foreach語句也是一樣。就是通過多次調用GetEnumerator和movenext方法來完成內部邏輯。因此查詢表達式中的所有邏輯都可以用運算符流語法來書寫。鞋面這個語句是上面的查詢表達式經編譯器編譯後的結果:
IEnumerable<string> query = names.Where (n => n.Contains ("a"))
.OrderBy (n => n.Length)
.Select (n => n.ToUpper());

Where、Orderby、Select這些運算符(我覺得是說的是查詢表達式中的關鍵字)在執行前被解析成運算符流語法中相對的運算符,能達到這個效果,是因爲這些關鍵字綁定了Enumerbale類中對應的查詢語法,如何知道綁定到哪個方法呢?在一開始導入了System.Linq命名空間,並且輸入集合names也實現了Enumerable<string>接口,這時使用Where、Orderby和Select這些運算符時,編譯器就會依次找到Enumerable類中綁定的方法,最終完成查詢的是這些方法而不是運算符,只是以一種更抽象的方式定義查詢表達式,這種做法不僅讓代碼更易懂,而且還可以在執行的時候判斷到底需要調用哪個類中的Where和Select方法。還有其他類也實現了這些方法,例如下面要講到的Queryable類。
如果我們從代碼中刪除對System.linq命名空間的引用,那麼查詢表達式就不能順利編譯,因爲編譯器在編譯where、orderby和select的時候,找不到與之對應的方法去綁定,編譯器需要的是可以進行綁定的方法。這時需要導入命名空間,或者爲每個查詢運算符實現一個相應的方法。
這裏寫圖片描述

範圍變量

在之前用到的LINQ表達式中,緊跟在from關鍵字之後標識符n實際上是一個範圍變量。範圍變量指向當前序列中藥進行操作的元素。
在之前的示例中,在每一個查詢子句中都有範圍變量,每個範圍變量都會在各個子句中被重新定義,後面的子句並沒有重用前面子句中的範圍變量,下面是一個簡單示例:

from n in names // 這裏的n是範圍變量
where n.Contains ("a") // n = from裏的n
orderby n.Length // n = 經where字句篩選過的n
select n.ToUpper() // n = 使用經orderby排序過的集合中的n

經過上面的解釋,就很容易理解編譯器將LINQ表達式轉換成運算符流形式後的代碼是:

names.Where (n => n.Contains ("a")) // 私有的局域變量
.OrderBy (n => n.Length) // 一個新的私有局域變量
.Select (n => n.ToUpper()) // 又一個

正如我們看到的,在每個子查詢的Lambda表達式中,n都會被重新定義。

  • 如果有必要,在查詢中可以將結果集暫存於某個變量中作爲中間結果集,然後再對這個中間結果集進行新的查詢。要定義這種存儲中間結果的變量,需要使用下面幾個子句:
    ①let
    ②into
    ③一個新的from子句
    ④join
    我們會在後面的內容中介紹這幾個字句的使用方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章