LINQ之路 7:子查詢、創建策略和數據轉換
在前面的系列中,我們已經討論了LINQ簡單查詢的大部分特性,了解了LINQ的支持計術和語法形式。至此,我們應該可以創建出大部分相對簡單的LINQ查詢。在本篇中,除了對前面的知識做個簡單的總結,還會介紹幾種創建更復雜查詢的方式,讓我們在面對更復雜的場景時也能輕松面對,包括:子查詢、創建策略和數據轉換。
子查詢
在創建一個復雜的查詢時,通常我們需要用到子查詢。相信大家都記得SQL查詢里的子查詢,在創建LINQ查詢時也是如此。在LINQ中,對于方法語法,一個子查詢包含在另外一個查詢的lambda表達式中,對于查詢表達式語法來講,所有不是from子句中引用的查詢都是子查詢。
下面的查詢使用子查詢來對last name進行排序,語句中的n.Split().Last()就是一個子查詢:
string[] names = { "David Tim", "Tony Sin", "Rager Witers" };
IEnumerable<string> query = names.OrderBy(n => n.Split().Last());
子查詢的作用域限定在當前的lambda表達式中,并且可以引用外部lambda表達式的參數(查詢表達式的范圍變量)。
下面的查詢獲取所有長度最短的名字(注意:可能有多個):
static void TestSubQuery()
{
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
// 獲取所有長度最短的名字(注意:可能有多個)
IEnumerable<string> outQuery = names
.Where(n => n.Length == names // 感謝A_明~堅持的指正,這里應該為==
.OrderBy(n2 => n2.Length)
.Select(n2 => n2.Length).First()); // Tom, Jay"
// 與上面方法語法等價的查詢表達式
IEnumerable<string> outQuery2 =
from n in names
where n.Length == // 感謝A_明~堅持的指正,這里應該為==
(from n2 in names orderby n2.Length select n2.Length).First()
select n;
// 我們可以使用Min查詢運算符來簡化
IEnumerable<string> outQuery2 =
from n in names
where n.Length == names.Min(n2 => n2.Length)
select n;
}
因為外部范圍變量在子查詢的作用域內,所以我們不能再次使用n作為內部查詢的范圍變量。
一個子查詢在包含它的lambda表達式執行時被執行,這意味著子查詢的執行取決于外部查詢。需要注意的是:本地查詢(LINQ to Objects)和解釋查詢(LIQN to SQL)對于子查詢的處理方式是不一樣的。對于本地查詢,對于外部查詢的每一次循環,子查詢都會被重新執行一次。在稍后“解釋查詢”一篇中, 我們會看到,外部查詢和子查詢是作為一個單元進行處理的,這樣,只需一次到遠程數據源(如數據庫)的連接。所以上面的例子對于一個數據庫查詢來說非常適合,但對于一個內存中的集合來說卻效率低下,這時我們可以把子查詢分離出來對讓它只執行一次(這樣它不再是一個子查詢)。
int shortest = names.Min(n => n.Length);
IEnumerable<string> query = from n in names
where n.Length == shortest
select n;
在延遲執行一篇中,我們說到元素和集合運算符如First和Count會讓一個查詢立即執行。但對一個子查詢來說,即使是元素和集合運算符也不會改變外部查詢延遲執行的特性。這是因為,不管是對本地查詢還是通過表達式樹訪問的解釋查詢,子查詢是間接調用的。
LINQ查詢創建策略
通過前面幾篇的討論學習,我們已經了解了怎么去寫一個比較簡單的LINQ查詢,也知道了創建LINQ查詢的兩種方式:方法語法和查詢表達式。在這里,我們會描述三種創建復雜LINQ查詢的創建策略:
漸進式創建查詢
漸進式創建查詢就是通過鏈接查詢運算符的方式來創建LINQ查詢。因為每一個查詢運算符返回一個裝飾者sequence,所以我們可以在其之上繼續調用其它查詢運算符。使用這種方式有如下幾個優點:
- 使得查詢易于編寫
- 我們可以根據條件來決定是否調用某個查詢運算符,如:if (includeFilter) query = query.Where(…)
漸進的方式通常是對查詢的創建有益的,考慮如下的例子:我們需要在名字列表中去除所有名字的元音字母,然后對長度大于2的名字進行排序。在方法語法中,我們可以在一個表達式中完成這個查詢:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names
.Select(n => n.Replace("a", "").Replace("e", "").Replace("i", "")
.Replace("o", "").Replace("u", ""))
.Where(n => n.Length > 2)
.OrderBy(n => n); // Result: Dck, Hrry, Mry
如果直接將上面的query改寫成查詢表達式語法,我們將會遇到麻煩,這時因為查詢表達式要求要以Select或Group結束。但在上面的查詢中,我們需要先做Select(結果投影)去除元音字母,再做過濾和排序。如果把Select直接放到后面,那么結果將會被改變。幸運的是,我們還是有辦法讓查詢表達式來完成上面的工作,得到我們期望的結果。 第一種方式就是查詢表達式的漸進式(分步)查詢:
IEnumerable<string> query =
from n in names
select n.Replace("a", "").Replace("e", "").Replace("i", "")
.Replace("o", "").Replace("u", "");
query = from n in query
where n.Length > 2
orderby n
select n; // Result: Dck, Hrry, Mry
into關鍵字
在我們前面查詢表達式的例子中,select關鍵字的出現也就意味著查詢的結束了。而into關鍵字讓我們在結果投影之后還可以繼續我們的查詢,它是對分步構建查詢表達式的一種簡寫方式。現在我們可以使用into關鍵字來重寫上例中的查詢:
IEnumerable<string> query =
from n in names
select n.Replace("a", "").Replace("e", "").Replace("i", "")
.Replace("o", "").Replace("u", "")
into noVowel
where noVowel.Length > 2
orderby noVowel
select noVowel; // Result: Dck, Hrry, Mry
我們只能在select和group子句后面使用into關鍵字,它會重新開始一個查詢,讓我們可以繼續引入where, orderby和select子句。盡管表面上看,我們重新創建了一個新的查詢,但當上面的查詢被翻譯成方法語法時,它只是一個查詢,一個鏈接了多個運算符的查詢,所以上面的寫法不會造成性能問題。
需要注意的是,所有的查詢變量在into關鍵字之后都不再可見,下面的例子就說明了這一點:
var query =
from n1 in names
select n1.ToUpper()
into n2 //into之后只有n2可見
where n1.Contains("x") //Error: n1不可見
select n2;
要理解其原因,我們只要看看它編譯器為它翻譯成對應的方法語法就能知曉:
var query = names
.Select(n1 => n1.ToUpper())
.Where(n2 => n1.Contains("x")); //Error: n1不再可見,lambda表達式中只有n2
包裝查詢
漸進式查詢創建方式可以通過在一個查詢中嵌入另一個查詢來改寫,這樣可以把多個查詢組合成單個查詢,即:
var tempQuery = tempQueryExpr
var finalQuery = from … in (tempQuery)
可以改寫為:
var query = from … in (tempQueryExpr)
上面兩種方式以及into關鍵字的工作方式是一樣的,編譯器都會把他們翻譯成一個鏈接查詢運算符。請看下面的示例:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
// 漸進式查詢(Progressive query building)
IEnumerable<string> query =
from n in names
select Regex.Replace(n, "[aeiou]", "");
query = from n in query where n.Length > 2 orderby n select n;
// 用包裝查詢方式進行改寫(Wrapping Queries)
IEnumerable<string> query2 =
from n1 in
(
from n2 in names
select Regex.Replace(n2, "[aeiou]", "")
)
where n1.Length > 2
orderby n1
select n1;
// 與上面等價的方法語法
IEnumerable<string> query3 = names
.Select(n => Regex.Replace(n, "[aeiou]", ""))
.Where(n => n.Length > 2)
.OrderBy(n => n);
數據轉換
LINQ中的數據轉換,也叫結果投影,是指LINQ查詢select的輸出。到目前為止,我們還只是看到了輸出單個標量元素的示例。通過使用對象初始化器,我們可以輸出更為復雜的結果類型。比如下面的示例,當我們在把姓名中的元音字母去掉之后,我還需要保存姓名的原始版本:
class TempProjectionItem
{
public string Original;
public string Vowelless;
}
static void TestProjectionStrategy()
{
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<TempProjectionItem> temp =
from n in names
select new TempProjectionItem
{
Original = n,
Vowelless = Regex.Replace(n, "[aeiou]", "")
};
//我們可以繼續在結果中查詢
IEnumerable<string> query =
from item in temp
where item.Vowelless.Length > 2 //按去除元音字母版本過濾
select item.Original; //結果為姓名原始版本
}
匿名類型
上面我們自己定義了類型TempProjectionItem來存放查詢的結果。通過使用匿名類型,我們可以省去這種中間類型的定義,而由編譯器來幫我們完成:
var intermediate = from n in names
select new
{
Original = n,
Vowelless = Regex.Replace(n, "[aeiou]", "")
};
IEnumerable<string> query = from item in intermediate
where item.Vowelless.Length > 2
select item.Original;
需要注意的是,因為匿名類型的確切類型名是由編譯器自動產生的,因此intermediate的類型為:IEnumerable <random-compiler-produced-name> 。我們來聲明這種類型的唯一方式就是使用var關鍵字,這時,var不只是更加簡潔,而且也是必需的手段。
let關鍵字
let關鍵字讓我們可以在保持范圍變量的同時引入新的查詢變量。比如上面的示例,我們可以用let關鍵字作如下改寫:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
var query = from n in names
let Vowelless = Regex.Replace(n, "[aeiou]", "")
where Vowelless.Length > 2
select n; //正是因為使用了let,此時n仍然可見
let關鍵字非常靈活和方便,就像例子看到的那樣。而且,我們可以使用多個let關鍵字,并且后面的 let表達式可以引用前一個let關鍵字引入的變量。
本系列LINQ之路文章到此已經對LINQ to Objects進行了比較詳細的討論,接下去的打算是對解釋查詢(LINQ to SQL, LINQ to XML等)以及更多的查詢運算符進行討論和學習。希望系列文章能對閱者有些幫助,也期待大家的意見和提議^_^。

浙公網安備 33010602011771號