應(yīng)用程序還需要操作存儲(chǔ)在其他數(shù)據(jù)源(如SQL數(shù)據(jù)庫或XML文件)中的數(shù)據(jù),甚至通過Web服務(wù)訪問它們。傳統(tǒng)上,查詢這些數(shù)據(jù)源時(shí)需要使用不同的語法,且在編譯期間不進(jìn)行類型檢查。
一、LINQ
在.NET Framework中,查詢表達(dá)式是一組統(tǒng)稱為語言集成查詢(LINQ)的技術(shù)的一部分,將查詢功能直接集成到了C#語言中。LINQ是.NET Framework 3.5新增的,它提供了適用于所有數(shù)據(jù)源(SQL數(shù)據(jù)庫、XML文檔、Web服務(wù)、ADO.NET數(shù)據(jù)庫以及任何支持接口IEnumberable或IEnumerable<T>的集合)的查詢語言,從而避免了操作數(shù)據(jù)和對(duì)象時(shí)經(jīng)常出現(xiàn)的語言不匹配問題。
LINQ讓查詢變成了基本語言構(gòu)造,就像算術(shù)運(yùn)算和流程控制語句是C#基本概念一樣。LINQ 查詢將重點(diǎn)放在常用的操作而不是數(shù)據(jù)結(jié)構(gòu)上,能夠以一致的方式從任何支持的數(shù)據(jù)源檢索數(shù)據(jù),并對(duì)其進(jìn)行轉(zhuǎn)換。
與SQL(Structured Query Language,結(jié)構(gòu)化查詢語言)查詢相比,LINQ查詢的語法相同,使用的一些關(guān)鍵字相同,提供的很多優(yōu)點(diǎn)也相同。您可隨便修改被查詢的底層數(shù)據(jù)的結(jié)構(gòu),而不會(huì)導(dǎo)致需要修改查詢。SQL只能用于操作關(guān)系型數(shù)據(jù),而LINQ支持的數(shù)據(jù)結(jié)構(gòu)要多得多。
如下代碼是一個(gè)用于Contact對(duì)象集合的查詢
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
var result = from contact in contacts select contact.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
這個(gè)簡(jiǎn)單的查詢演示了 C#語言支持的聲明性語法(declartive syntax,也叫 query comprehension syntax)。這種語法讓您能夠使用類似于SQL查詢的語法編寫查詢,靈活性和表達(dá)能力都極強(qiáng)。雖然查詢表達(dá)式中所有的變量都是強(qiáng)類型的,但是在大多數(shù)情況下,不需要顯式地指定類型,因?yàn)榫幾g器能夠推斷出來。
ps:LINQ查詢語法
如果您熟悉SQL,就不會(huì)對(duì)LINQ使用的查詢語法感到陌生。最明顯的差別是,from運(yùn)算符位于select運(yùn)算符的前面,而不像SQL中那樣位于后面。
1.1 選擇數(shù)據(jù)
雖然上述所示的代碼看起來可能很簡(jiǎn)單,但是實(shí)際上涉及的內(nèi)容很多。首先應(yīng)注意到的是,使用了一個(gè)類型將隱式確定的變量result,其類型實(shí)際上是IEnumerable<string>。查詢表達(dá)式(賦值運(yùn)算符右邊的代碼)的結(jié)果為查詢,而不是查詢的結(jié)果。select 子句返回一個(gè)對(duì)象,該對(duì)象表示對(duì)一個(gè)序列( contacts 列表)執(zhí)行投影操作的結(jié)果(一系列contact.FirstName值)。由于結(jié)果是一系列字符串,因此result必然是由字符串組成的可枚舉集合。當(dāng)前,并不會(huì)檢索數(shù)據(jù),而只是返回一個(gè)可枚舉集合,以后再將數(shù)據(jù)取回。
這個(gè)查詢相當(dāng)于說,從contacts指定的數(shù)據(jù)源中,選擇每個(gè)元素(contact)的FirstName字段。from子句中指定的變量contact類似于foreach語句中的迭代變量,它是一個(gè)只讀局部變量,作用域?yàn)椴樵儽磉_(dá)式。in子句指定了要從中查詢?cè)氐臄?shù)據(jù)源,而select子句指定在迭代期間只選擇每個(gè)元素的contact.FirstName字段。
選擇單個(gè)字段時(shí),這種語法的效果很好,但通常需要選擇多個(gè)字段,甚至以某種方式對(duì)數(shù)據(jù)進(jìn)行變換,如合并字段。所幸的是,LINQ通過類似的語法提供了這樣的支持。實(shí)際上,有多種方法執(zhí)行這些類型的選擇。
第一種方法是在select子句中拼接這些字段,這樣將只返回一個(gè)字段,如下代碼所示:
var result = from contact in contacts select contact.FirstName + " " + contact.LastName;
foreach(var name in result)
{
Console.WriteLine(name);
}
顯然,這種選擇方式只適用于有限的情形。一種更靈活的方法是返回多個(gè)字段,即返回一個(gè)數(shù)據(jù)子集,如下所示:
var result = from contact in contacts
select new
{
Name = contact.LastName + ", " + contact.FirstName;
DateOfBirth = contact.DateOfBirth
};
foreach(var contact in result)
{
Console.WriteLine("{0} born on {1}", contact.Name, contact.DateOfBirth);
}
這里返回的仍是IEnumberable,但其類型是什么呢?如果查看程序中的select子句,就會(huì)發(fā)現(xiàn)它返回了一種新類型,其中包含字段contact.FirstName和contact.LastName的值。這實(shí)際上是一個(gè)匿名類型(anonymous type),它包含屬性Name和DateOfBirth。這種類型之所以是匿名的,是因?yàn)樗鼪]有名稱。無需顯式地聲明與返回值對(duì)應(yīng)的新類型,編譯器會(huì)自動(dòng)生成。
ps:匿名類型
以這種方式創(chuàng)建匿名類型是LINQ工作方式的核心,如果沒有var提供的引用類型,這根本不可能。
1.2 篩選數(shù)據(jù)
選擇數(shù)據(jù)很重要,但以這種方式選擇數(shù)據(jù)時(shí),無法指定要返回哪些數(shù)據(jù)。SQL 提供了where子句,同樣,LINQ也提供了where子句,它返回一個(gè)可枚舉的集合,其中包含符合指定條件的元素。如下代碼再上一個(gè)示例的查詢中添加了一個(gè)where子句,將結(jié)果限定為StateProvince字段為FL的聯(lián)系人。
var result = from contact in contacts
whrere contact.StateProvince == "FL"
select new {customer.FirstName, customer.LastName};
foreach(var name in result)
{
Console.WriteLine(name.FirstName + " " + name.LastName);
}
首先執(zhí)行where子句,再對(duì)得到的可枚舉集合執(zhí)行select子句,其結(jié)果為一個(gè)包含屬性FirstName和LastName的匿名類型。
1.3 對(duì)數(shù)據(jù)進(jìn)行分組和排序
為支持更復(fù)雜的情形,如對(duì)返回的數(shù)據(jù)進(jìn)行排序或分組,LINQ 提供了 orderby 子句和group 子句。可將數(shù)據(jù)按升序(從最小到最大)或降序(從最大到最?。┡帕校捎谏蚴悄J(rèn)設(shè)置,因此不需要指定升序。如下程序?qū)⒔Y(jié)果按字段LastName排序。
var result = from contact in contacts
oderby contact.LastName
select contact.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
可根據(jù)多個(gè)字段進(jìn)行排序,并混合使用升序和降序,這將創(chuàng)建出非常復(fù)雜的 orderby 語句,如下代碼所示:
var result = from contact in contacts
oderby
contact.LastName ascending,
contact.FirstName descending
select customer.FirstName;
foreach(var name in result)
{
Console.WriteLine(name);
}
將數(shù)據(jù)分組的方法與此類似,但將使用group子句替換select子句。對(duì)數(shù)據(jù)分組時(shí)的不同之處在于,返回的結(jié)果為由 IGrouping<TKey, TElement>對(duì)象組成的 IEnumerable,可將其視為由列表組成的列表。這要求使用兩條嵌套的foreach語句來訪問結(jié)果。
如下代碼是一個(gè)使用group子句的LINQ查詢
var result = from contact in contacts
group contact by contact.LastName[0];
foreach(var group in result){
Console.WriteLine("Last names starting width {0}", group.key);
foreach(var name in result)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
如果需要引用分組操作的結(jié)果,就可創(chuàng)建一個(gè)標(biāo)識(shí)符,使用關(guān)鍵字into將查詢結(jié)果存儲(chǔ)到該標(biāo)識(shí)符中,并對(duì)其做進(jìn)一步查詢。這種組合方式稱為查詢延續(xù)(continuation)。
如下代碼演示了一個(gè)使用group和into的LINQ查詢:
var result = from contact in contacts
group contact by contact.LastName[0] into namesGroup
where namesGroup.Count() > 2
select namesGroup;
foreach(var group in result)
{
Console.WriteLine("Last names starting width {0}", group.key);
foreach(var name in result)
{
Console.WriteLine(name);
}
Console.WriteLine();
}
1.4 聯(lián)接數(shù)據(jù)
LINQ 還能夠合并多個(gè)數(shù)據(jù)源,這是通過利用一個(gè)或多個(gè)都有的字段將它們聯(lián)接起來實(shí)現(xiàn)的。查詢多個(gè)沒有直接關(guān)系的數(shù)據(jù)源時(shí),聯(lián)接數(shù)據(jù)很重要。SQL支持使用很多運(yùn)算符進(jìn)行聯(lián)接,但LINQ聯(lián)接基于相等性。
前面的示例只使用了Contact類,要執(zhí)行聯(lián)接操作,至少需要兩個(gè)類。程序清單12.9再次列出了Contact類,還列出了新增的JournalEntry類。繼續(xù)假設(shè)通過調(diào)用GetContacts填充了contacts列表,并通過調(diào)用GetJournalEntries填充了journal列表。
如下代碼演示了Contact和JournalEntry類
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
}
class JournalEntry
{
public int Id { get; set; }
public int ContactId { get; set; }
public string Description { get; set; }
public string EntryType { get; set; }
public DateTime Date { get; set; }
}
IEnumberable<Contact> contacts = GetContacts();
IEnumberable<JournalEntry> journal = GetJournalEntries();
在LINQ中,最簡(jiǎn)單的聯(lián)接查詢與SQL內(nèi)聯(lián)接等效,這種查詢使用join子句。SQL聯(lián)接可使用很多不同的運(yùn)算符,而LINQ聯(lián)接只能使用相等運(yùn)算符,稱之為相等聯(lián)接(equijoin)。
如下示例的查詢使用Contact.ID和JournalEntry.ContactId作為聯(lián)接鍵,將一個(gè)由Contact對(duì)象組成的列表和一個(gè)由JournalEntry對(duì)象組成的列表聯(lián)接起來:
var result =
from contact in contacts
join journalEntry in journal
on contact.Id equals journalEntry.ContactId
select new
{
contact.FirstName,
contact.LastName,
journalEntry.Date,
journalEntry.EntryType,
journalEntry.Description
};
如上所示的 join子句創(chuàng)建了一個(gè)名為 journalEntry的范圍變量(range ariable),其類型為JournalEntry;然后使用equals運(yùn)算符將兩個(gè)數(shù)據(jù)源聯(lián)接起來。
LINQ還支持分組聯(lián)接概念,而SQL沒有與之對(duì)應(yīng)的查詢。分組聯(lián)接使用關(guān)鍵字into,其結(jié)果為層次結(jié)構(gòu)。就像使用group子句時(shí)那樣,需要使用嵌套foreach語句來訪問結(jié)果。
ps:順序很重要
使用LINQ聯(lián)接時(shí),順序很重要。被聯(lián)接的數(shù)據(jù)源必須位于equals運(yùn)算符左邊,而聯(lián)接數(shù)據(jù)源必須位于右邊。在這個(gè)示例中,contacts是被聯(lián)接的數(shù)據(jù)源,而journal是聯(lián)接數(shù)據(jù)源。
所幸的是,順序不正確時(shí),編譯器能夠捕獲并生成編譯錯(cuò)誤。如果交換join子句中的參數(shù),將出現(xiàn)下面的編譯錯(cuò)誤:
名稱“Journalentry”不在“equals”左側(cè)的范圍內(nèi)。請(qǐng)考慮交換“equals”兩側(cè)的表達(dá)式。
需要注意的另一個(gè)重點(diǎn)是,join子句使用運(yùn)算符equals,它與相等運(yùn)算符(==)不完全相同。
如下代碼的查詢聯(lián)接contacts和journal,并將結(jié)果按聯(lián)系人姓名分組。在返回的結(jié)果集中,每個(gè)元素都包含一個(gè)由 JournalEntry 組成的可枚舉集合,該集合由返回的匿名類型的JournalEntries屬性表示。
var result =
from contact in contacts
join journalEntry in journal
on contact.Id equals journalEntry.ContactId
into journalGroups
select new
{
Name = contact.LastName + "," +contact.FirstName,
JournalEntries = journalGroups
};
1.5 數(shù)據(jù)平坦化
雖然選擇和聯(lián)接數(shù)據(jù)時(shí),返回的數(shù)據(jù)是合適的,但層次型數(shù)據(jù)使用起來比較繁瑣。LINQ能夠創(chuàng)建返回平坦化數(shù)據(jù)的查詢,就像查詢SQL數(shù)據(jù)源一樣。
假設(shè)對(duì)Contact和JournalEntry類進(jìn)行了修改:在Contact類中添加了一個(gè)Journal字段,其類型為List<JournalEntries>,并刪除了JournalEntry類的屬性ContactId,如下所示
class Contact
{
public int Id { get; set; }
public string Company { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
public List<JournalEntries> Journal;
}
class JournalEntry
{
public int Id { get; set; }
public string Description { get; set; }
public string EntryType { get; set; }
public DateTime Date { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
在這種情況下,可使用查詢檢索特定聯(lián)系人的JournalEntry列表,如下所示:
var result =
from contact in contacts
where contact.id == 1
select contact.Journal;
foreach(var item in result)
{
foreach(var journalEntry in item)
{
Console.WriteLine(journalEntry);
}
}
雖然這樣可行,也返回了所需的結(jié)果,但是仍需使用嵌套 foreach 語句來訪問結(jié)果。所幸的是,LINQ 支持從多個(gè)數(shù)據(jù)源選擇數(shù)據(jù),從而提供了一種返回平坦化數(shù)據(jù)的查詢語法。如下程序演示了這種查詢語法,它使用多個(gè)from子句,使得訪問數(shù)據(jù)時(shí)只需一條foreach語句。
var result =
from contact in contacts
from journalEntry in contact.Journal
where contact.id == 1
select contact.Journal;
foreach(var item in result)
{
Console.WriteLine(journalEntry);
}
二、標(biāo)準(zhǔn)查詢運(yùn)算符方法
前面介紹的所有查詢都使用聲明性查詢語法,但也可使用標(biāo)準(zhǔn)查詢運(yùn)算符方法來編寫這些查詢。標(biāo)準(zhǔn)查詢運(yùn)算符方法實(shí)際上是命名空間System.Linq中定義的Enumerable類的擴(kuò)展方法。對(duì)于使用聲明性語法的查詢表達(dá)式,編譯器將其轉(zhuǎn)換為等價(jià)的查詢運(yùn)算符方法調(diào)用。
使用using語句包含命名空間System.Linq后,對(duì)于任何實(shí)現(xiàn)了接口IEnumberable<T>的類,智能感知列表都可包含標(biāo)準(zhǔn)查詢運(yùn)算符方法。
雖然聲明性查詢語法幾乎支持所有的查詢操作,但是也有一些操作(如Count和Max)沒有對(duì)應(yīng)的查詢語法,必須使用方法調(diào)用來表示。由于每個(gè)方法調(diào)用都返回IEnumerable,因此通過串接方法調(diào)用,可編寫出復(fù)雜的查詢。編譯聲明性查詢表達(dá)式時(shí),編譯器就是這樣做的。
ps:使用聲明性語法還是方法語法
使用聲明性語法還是方法語法因人而異,這取決于個(gè)人認(rèn)為哪種語法更容易理解。無論使用哪種語法,執(zhí)行查詢得到的結(jié)果都相同。
如下示例演示了使用方法語法的LINQ查詢
var result = contacts.
Where(contact => contact.StateProvince == "FL").
Select(contact => new { contact.FirstName, contact.LastName });
foreach(var name in result)
{
Console.WriteLine(name.FirstName + " " + name.LastName);
}
三、Lambda
上述示例中,傳遞給方法Where和Select的參數(shù)看起來與以前使用過的參數(shù)不同。這些參數(shù)實(shí)際上包含的是代碼,而不是數(shù)據(jù)。之前介紹過委托和匿名方法,委托能夠?qū)⒁粋€(gè)方法作為參數(shù)傳遞給另一個(gè)方法,而匿名方法能夠編寫未命名的內(nèi)聯(lián)語句塊,這些語句塊將在調(diào)用委托時(shí)執(zhí)行。
Lambda 結(jié)合使用了這兩個(gè)概念,它是可包含表達(dá)式和語句的匿名函數(shù)。通過使用Lambda,可以更方便、更簡(jiǎn)潔的方式編寫這樣的代碼,即正常情況下需要使用匿名方法或泛型委托進(jìn)行編寫。
ps:Lambda和委托
由于Lambda是編寫委托的更簡(jiǎn)潔方式,因此可在通常需要使用委托的任何地方使用它們。所以,Lambda的形參類型必須與相應(yīng)的委托類型完全相同,返回類型也必須隱式地轉(zhuǎn)換為委托的返回類型。
雖然 Lambda 沒有類型,但是它們可隱式地轉(zhuǎn)換為任何兼容的委托類型。正是這種隱式轉(zhuǎn)換讓您無需顯式賦值就能夠傳遞它們。
在C#中,Lambda使用Lambda運(yùn)算符(=>)。在方法調(diào)用中,該運(yùn)算符左邊指定了形參列表,而該運(yùn)算符右邊為方法體。匿名方法的所有限制也適用于Lambda。
在上述示例中,實(shí)參 contact => contact.StateProvince == "FL"的意思為,這是一個(gè)以 contact為參數(shù)的函數(shù),其返回值為表達(dá)式 contact.StateProvince == "FL"的結(jié)果。
ps:捕獲的變量和定義的變量
Lambda還能捕獲變量,這可以是Lambda所屬方法的局部變量或參數(shù)。這使得可在Lambda體內(nèi)通過名稱訪問捕獲的變量。如果捕獲是局部變量,那么必須賦值后才能在Lambda中使用它。ref或out參數(shù)無法捕獲。
然而,需要注意的是,對(duì)于Lambda捕獲的變量,在引用它的委托超出作用域前,垃圾收集器將不會(huì)收集它們。
Lambda中聲明的變量在Lambda所屬方法內(nèi)不可見,輸入?yún)?shù)也如此,因此可在多個(gè)Lambda中使用同一個(gè)標(biāo)識(shí)符。
表達(dá)式Lambda
在Lambda中,如果運(yùn)算符右邊為表達(dá)式,該Lambda就為表達(dá)式Lambda,它返回該表達(dá)式的結(jié)果。表達(dá)式Lambda的基本格式如下:
(input parameters) => expressions
如果只有一個(gè)輸入?yún)?shù),那么括號(hào)是可選擇的;否則(包括沒有參數(shù)時(shí)),括號(hào)將是必不可少的。
就像泛型方法可推斷其類型參數(shù)的類型一樣,Lambda 也能推斷其輸入?yún)?shù)的類型。如果編譯器無法推斷出類型,您就可以顯式地指定類型。
如果將表達(dá)式 Lambda 的表達(dá)式部分視為方法體,那么表達(dá)式 Lambda 包含一條隱式的return語句,它返回表達(dá)式的結(jié)果。
ps:包含方法調(diào)用的表達(dá)式Lambda
大部分示例都在右邊使用了方法,但是如果創(chuàng)建的Lambda將用于其他域,如SQL Server,就不應(yīng)使用方法調(diào)用,因?yàn)樗鼈冊(cè)?NET Framework公共語言運(yùn)行時(shí)外面沒有意義。
語句Lambda
在 Lambda 的右邊,可使用一條或多條用大括號(hào)括起的語句,這種 Lambda 稱為語句Lambda。語句Lambda的基本形式如下:
(input parameters) => { statement; }
與表達(dá)式 Lambda 一樣,如果只有一個(gè)輸入?yún)?shù),那么括號(hào)是可選的;否則,括號(hào)就必不可少。語句Lambda也遵循同樣的類型推斷規(guī)則。
雖然表達(dá)式Lambda包含一條隱式的return語句,但是語句Lambda沒有,您必須在語句Lambda中顯式地指定return語句。return語句只導(dǎo)致從Lambda表示的隱式方法返回,而不會(huì)導(dǎo)致從Lambda所屬的方法返回。
語句Lambda不能包含這樣的goto、break和continue語句,即其跳轉(zhuǎn)目標(biāo)在Lambda外。同樣,作用域規(guī)則禁止從嵌套Lambda分支到外部Lambda。
預(yù)定義的委托
雖然 Lambda 是 LINQ 的有機(jī)組成部分,但是可將其用于任何可使用委托的地方。因此,.NET Framework提供了很多預(yù)定義的委托,可將其作為方法參數(shù)進(jìn)行傳遞,而無需首先聲明顯式的委托類型。
由于返回Boolean值的委托很常見,因此.NET Framework定義了一個(gè)Predicate<in T>委托,Array和List<T>類的很多方法都使用它。
Predicate<T>定義了一個(gè)總是返回Boolean值的委托,而Func系列委托封裝了有指定返回值,且接受0~16個(gè)輸入?yún)?shù)的方法。
Predicate<T>和 Func 系列委托都有返回值,但是 Action 系列委托表示返回類型為 void的方法。就像Func系列委托一樣,Action系列委托也接受0~16個(gè)輸入?yún)?shù)。
四、延遲執(zhí)行
不同于眾多傳統(tǒng)的數(shù)據(jù)查詢技術(shù),LINQ 查詢要等到實(shí)際迭代其結(jié)果時(shí)才執(zhí)行,稱之為延遲執(zhí)行(lazy evaluation)。其優(yōu)點(diǎn)之一是,在指定查詢和檢索查詢指定的數(shù)據(jù)之間,可修改原始集合中的數(shù)據(jù)。這意味著您獲得的數(shù)據(jù)總是最新的。
雖然LINQ首選延遲執(zhí)行,但是使用了任何聚合函數(shù)的查詢都必須先迭代所有元素。這些函數(shù)(如Count、Max、Average和First)都返回一個(gè)值,并且無需使用顯式foreach語句就能執(zhí)行。
ps:延遲執(zhí)行和串接查詢
延遲執(zhí)行的另一個(gè)優(yōu)點(diǎn)是,讓您能夠串接查詢,從而提供編碼效率。由于查詢對(duì)象表示的是查詢,而不是查詢的結(jié)果,因此可輕松地串接或重用它們,而不會(huì)導(dǎo)致開銷高昂的數(shù)據(jù)取回操作。
也可強(qiáng)制查詢立刻執(zhí)行,這有時(shí)稱為貪婪執(zhí)行(greedy evaluation)。為此,可在查詢表達(dá)式后面,緊接著放置一條foreach語句,也可調(diào)用方法ToList 或ToArray。方法ToList 和ToArray還可用于將數(shù)據(jù)緩存到一個(gè)集合對(duì)象中。