學習和使用C#已經有2個月了,在這兩個月的學習中,深刻體會到,C#這門語言還真不適合編程初學者學習 ,因為它是吸取了很多其他語言,不僅是面向對象,還包括函數式語言的很多特性,導致它變成特性大爆炸的 語言。它的許多方面單獨拿出來講,就得是一本書的規模,而且還不一定讓人一下子明白。
LINQ,Language INtegrated Query,語言集成查詢,是其中一個非常重要的部分,有關它的功能增強, 貫穿了整個C#的發展。
先從基本的查詢表達式下手。
在講查詢表達式前,我們必須明白:查詢 表達式不僅僅是針對數據庫,它針對的是所有數據源,因為LINQ的意圖就是為所有數據源提供統一的訪問方式 。因為最近的項目使用的是LINQ to SQL,所以這裡只講LINQ to SQL。
查詢表達式非常像SQL語句,但 書寫方式卻是反過來:
var articles = from article in db.Articles where a.Name == "JIM" select article;
我們必須明白,查詢表達式返回的結果是一個序列,哪怕這個序列 只有一個元素。
正因為查詢表達式返回的是一個序列,才使得它的行為非常有趣。序列的基本特點就 是每次只取一個元素,這使得每個轉換都處理一個數據,而且只在結果序列的第一個元素被訪問的時候才會開 始執行查詢表達式。像是上面的數據流是這樣的:select轉換會在它返回的序列中的第一元素被訪問時,為該 元素調用where轉換,where轉換會返回數據列表中第一個元素,檢查這個謂詞(a.Name == "JIM") 是否匹配,再把該元素返回給select。
這就是查詢表達式的延遲執行,在它被創建的時候沒有處理任 何數據,只是在內存中生成了這個查詢的委托。這樣子是非常高效和靈活的,但並不是所有情況都適合,像是 reverse這類的操作,就要訪問整個數據源。所以,我們一般在返回另一個序列的操作中使用延遲執行,如果 是返回單一值就使用即時執行。
使用查詢表達式,首先就是聲明數據序列的數據源:
from article in db.Articles
where子句用來進行過濾。它會進入數據流中每個元素的謂詞,只有返回true 的元素才能出現在結果序列中。我們可以使用多個where子句,只有滿足所有謂詞的元素才能進入結果序列。
編譯器會將where子句轉換為帶有Lambda表達式的where方法調用,像是這樣:
where(article => article.Name == "JIM")
所以我們也可以直接使用Lambda表達式,它同樣是返回一 個序列:
var articles = db.Articles.Where(article => article)
使用Lambda表達式能使 我們的代碼的簡潔度大幅上升,這也是它為什麼會被引進C#中的原因之一。
select子句就是投影,相 信學過數據庫的同學一定非常熟悉。它同樣也會有對應的方法調用,應該說,幾乎所有的LINQ to SQL操作都 有對應的方法調用(因為編譯器就是講它們轉換為方法調用),所以接下來就不再講方法調用形式了。
前面講過,編譯器會將查詢表達式轉換為普通C#代碼的方法調用,以select為例,它並不會像我們預期的那樣 ,轉換為Enumerable.Select或者List<T>.Select,它就只是對代碼進行轉換,然後再尋找適當方法。 該方法的參數會是一個委托類型或者一個Expression<T>。為什麼轉換後的方法中的參數是一個Lambda 表達式呢?因為Lambda表達式可以被轉換為委托實例或者表達式樹,所以使用Lambda表達式就可以對應所有情 況。這種轉換並不依賴與特定類型,而只依賴與方法名稱和參數,也就是所謂的動態類型(Duck Typing)的編 譯時形式。
因為這樣,我們可以實現自己的LINQ提供器,但除非真的有特殊需要,一般我們都不需要 做到這點。
我們來看看查詢表達式中兩個最重要的組成:范圍變量和投影表達式。
from article in db.Articles
其中,article就是范圍變量,而:
select article
投影表 達式就使用了該范圍變量。編譯器在轉換的時候,Lambda表達式的左邊,參數名稱就是范圍變量,右邊來自於 投影表達式,所以,我們決不能這樣寫:
from article in db.Articles select person
從上面來看,范圍變量應該是隱式類型,編譯器會自己推斷它的具體類型。當然, 我們也可以使用顯式類型的范圍變量,像是下面這樣:
from Article article in db.Articles select article
其中,db.Articles是一個List<Article>。
這樣的情況是因為我們想 要在強類型的容器中進行查詢。這樣的機制能夠運行的保障源於一個方法:Cast()。Cast()會強制將類型轉換 為目標類型,如果無法轉換則會出錯。
上面的表達式會轉換為以下的代 碼:
list.Cast<Article>().Select(article => article);
為什麼需要我們對范圍變 量進行顯式聲明呢?因為Select方法只是IEnumerable<T>的擴展方法,而不是IEnumerable,所以我們 必須要顯式的聲明范圍變量的類型才能調用Select方法。
讓我們更加深入的研究一下投影表達式。
前面的Select其實是不做事的,但它也並不會返回源數據,查詢表達式的結果和源數據必須不一樣,否則我們 對結果進行的操作可能就會影響到源數據了。這樣的Select有一個專門的稱呼:退化查詢表達式。如果沒有其 他子句,像是where子句的調用,編譯器還是會為select表達式生成Select方法調用,但像是前面,結果生成 的表達式並沒有Select方法調用。
所有的查詢表達式都需要select或者group...by結尾,所以退化查 詢表達式才會誕生。
數據庫的操作經常需要對元素進行排序,所以我們接下來就講orderby子句。
orderby子句可以有多個排序規則,默認是升序,也可以選擇降序descending。我們來看一個例子:
from article in db.Articles orderby article.Age descending, article.Height select article;
編譯器生成的表達式如:
db.Articles.OrderbyDescending(article => article.Age).ThenBy(article => article.Height);
OrderBy對排序規則起決定作用,而 ThenBy只是在OrderBy的結果中進行排序,而且它本身就被定義為IOrderedEnumerable<T>的擴展方法, 這是OrderBy返回的類型,這注定我們無法單獨使用ThenBy(當然,ThenBy本身也返回該類型,這是為了進一步 的連鎖)。
最好只使用一個orderby子句,雖然理論上我們是可以使用多個。
前面的查詢表達都是最 基本的操作,對於一般的要求就已經足夠了,接下來講解比較復雜的查詢表達式。
剩下的查詢表達式 都會涉及到透明標識符。
使用透明標識符最簡單的應用就是let子句:
from article in db.Articles let length = article.Name.Length orderby length select new {Name = article.Name, Length = length};
上面的查詢表達式遠比之前要復雜得多了 !
let子句引入了新的范圍變量,它的值是基於其他范圍變量。但問題來了,Lambda表達式只會給select傳 遞一個參數!而我們這裡有兩個范圍變量!!所以,前面我們才會創建一個匿名類型來包含這兩個變量,於是 實際的查詢表達式是被轉換為這樣:
db.Articles.Select(article => new {article, length = article.Name.Length}) .OrderBy(z => z.length).Select(z => new {Name = article.Name, Length = z.length});
z這個名稱是編譯器隨機生成的,我們注意到,在let子句後的查詢表達式,凡是涉及 到length的地方,都會用z.length來代替。這就是透明標識符。
數據庫操作都會涉及到聯接,LINQ的聯接 有3種類型:
1.內聯接
內聯接涉及到兩個序列:鍵選擇器(應用於第二個序列的每個元素)和鍵 選擇器表達式(應用於第一個序列的每個元素)。聯接的結果是一個所有配對元素的序列,配對的規則是第一個 元素的鍵與第二個元素的鍵相同。
使用內聯接的方式如下:
from article in db.Articles join comment in db.Comments on article.Name equals comment.ArticleName select new { article.Name, comment.Content};
對於這兩個序列,右邊序列db.Comments會在左邊 序列db.Articles進行流處理的時候進行緩沖,所以當我們要把一個巨大的序列聯接到一個小序列上的時候, 應把小序列作為右邊序列。
如果我們想要過濾序列,最好是在內聯接之前對左邊序列進行過濾,這樣 查詢表達式就會更加簡單,對右邊序列進行過濾,可能就要使用嵌套查詢表達式,這樣可讀性很差。
2.分組聯接
分組聯接結果的每個元素由左邊序列的范圍變量的1個元素組成,由右邊序列的所 有匹配元素組成的序列顯示為一個新的范圍變量,該變量由into後面出現的標識符指定:
from article in db.Articles join comment in db.Comments on article.Name equals comment.ArticleName into comments select new { Article = article, Comments = comments};
Comments是一個嵌入序列,包含了匹配 article的所有comment。
內聯接和分組聯接最大的差異就是分組聯接在左邊序列和結果序列間存在一對一 的對應關系,即使左邊序列中的某些元素在右邊序列中沒有任何匹配的元素,會用空表示。分組聯接同樣要對 右邊序列進行緩沖,對左邊序列進行流處理。
3.交叉聯接
前面的聯接都是相等聯接(equijion) ,左邊序列中的元素和右邊序列進行匹配。但交叉聯接並不在序列間進行任何匹配操作,結果包含了每個可能 的元素對:
from article in db.Articles from comment in db.Comments select new { Article = article, Comment = comment};
交叉聯接的結果就是一個笛卡爾積,左邊 序列被聯接到右邊序列的每個元素中。
當然,我們必須對數據庫有足夠的了解才能明白上面的聯接是怎麼 回事,但正如我們在開頭強調的,C#是一門綜合性非常強大的語言,所以在學習它的特性前我們必須對相關知 識有足夠的了解。
討論完聯接後,我們接下來就是分組。
分組的實現非常簡單:
from article in db.Articles
where article.Name == "JIM"
group article by article.Age;
當然,我們可以對article.Author進行分組,但這時編譯器生成的表達式就非常有趣 了:
db.Articles.Where(article => article.Name == "JIM").GroupBy(article => article.Age,
article => article.Author);
GroupBy有更加復雜的版本,它們 提供的功能可能比使用查詢表達式還要更加強大。
如果打算對查詢結果進行更急處理,可以使用查詢延續 。查詢延續是把一個查詢表達式的結果用作另外一個查詢表達式的初始序列。像是這樣:
from article in db.Articles
where article.Name == "JIM"
group article by article.Age into grouped
select new { Age = grouped.Key,
count = grouped.Count()};
可怕的是,我們還能在 查詢延續後面繼續延續!
from article in db.Articles where article.Name == "JIM" group article by article.Age into grouped select new { Age = grouped.Key, count = grouped.Count()} into result orderby result.Count select result;
當然,使用查詢表達式必須注意查詢語句的復雜性,像是上面的例子就已經足夠復 雜了。
在代碼中使用查詢表達式必須注意,如果是復雜的情況,查詢表達式會非常復雜,這時使用查 詢表達式就不是一個好主意,所以,C#提供了很多方法調用來簡化這些操作。我們之間其中幾個比較常見的操 作。
First()返回的是滿足條件的第一個元素,而Single()返回的是滿足條件的唯一一個元素,但如果 有多個元素符合就會跑出錯誤。它們在取出元素的時候非常有用,但因為可能存在錯誤,所以最好使用 FirstOrDefault()和SingleOrDefault()來代替。Include()這個方法主要用於提取表中的子表中的元素,像是 Articles這個表,可以有一個List<Comment> Comments用於存放相關的comment,可以這樣:
var articles = db.Articles.Include("Comments").FirstOrDefault(c => c.Name == article.Name);
這樣我們就能提取出Comments中的元素了。我們要時刻記住,無論是查詢表達式還是 方法的調用,最後返回的結果都會是一個IQuerable<T>,是一個序列,而不是單個結果,哪怕這個序列 只有一個結果,所以,使用ToList()將其轉換為List<T>,就可以方便的使用List的方法對該序列進行 操作了。