進階基本知識
LINQ Enumerable 類別,第一篇。
Ken Getz

目錄
在最近的程式設計專案中,我需要建立一份隨機排序的整數清單,整數介於 1 和 100 之間。沒錯,一看就知道這是「資訊工程 101」的基本課題。一開始,我採用在 1985 年教書時的解答,其中使用許多迴圈和比較運算,然後產生隨機數字和許多陣列。但是,在這宣告式程式設計的時代,這樣的解答無法令我滿意。
幾經研究之後,我總算將問題簡化成只剩兩行程式碼:
|
Dim rnd As New System.Random()
Dim numbers = Enumerable.Range(1, 100). _
OrderBy(Function() rnd.Next)
|
執行這兩行程式碼後,numbers 變數就會含有介於 1 和 100 之間的隨機排序整數。
這個簡單又小巧的辦法需要仰賴 System.Linq.Enumerable 類別才能運作,這個類別提供一些共用的方法,稱為擴充方法,可讓您在任何實作 IEnumerable(Of T) 介面的類別中操作資料。此外,從這個範例中可以看出,這些方法還可以產生新的資料集合。
在本專欄與下回的專欄中,我會提供 Enumerable 類別的擴充方法範例,列出其成員的若干多載版本,同時也會利用各種資料結構來示範這些方法的功能。由於這些方法在許多情況下都可派上用場且非常好用,所以值得花一些時間來好好研究每一個方法。如此一來,當您需要操作集合時,就知道手上有哪些工具可用。
我建立了一個簡單的 Windows® 應用程式來做示範。在這個專案中,我加入了數個簡單的類別,分別代表 Customers (客戶)、Categories (分類) 及 Products (產品) 等資料,另外也加入一個叫做 SimpleDataContext 的類別,這個類別會從 SQL Server® Northwind 範例資料庫取得資料,然後填入這些類別的執行個體中。此外,這個類別還會公開 Customers、Categories 及 Products 這三個公用屬性,每一個屬性分別包含 Customer、Category 及 Product 執行個體的泛型清單 (雖然我可以用 LINQ to SQL 設計工具來建立「真實」的 DataContext 類別,但如果這樣做的話,我就無法示範 Enumerable 類別的擴充方法:當您在 LINQ to SQL 環境下呼叫上述方法時,實際是呼叫到 LINQ to SQL 提供者的 Queryable 類別的擴充方法,這些方法的簽章和這裡所說的方法相同。雖然這樣的範例也能有相同的表現,但卻沒有使用到 Enumerable 類別)。我強烈建議您下載範例應用程式,然後跟著文章在應用程式中操練。如果您能親自體驗一番,而不是只在這裡紙上談兵,相信您對程式碼會有更深入的理解。
為了方便討論這一長串的方法,我將這些方法稍微做一下合理的分組。您會研究到的方法包括建立、選取及擷取單一元素,在序列內進行篩選、轉換及定位,以及對資料序列進行排列、驗證、計算及執行集合 (Set) 作業。在接下來的各節中,您將試驗各種範例,這些範例會示範 Enumerable 類別的一個或多個方法。閱讀完這幾節之後,表示您已將 Enumerable 類別的所有方法瀏覽一遍,也已研究過更複雜方法的各種多載版本。
請注意,範例中通常允許 Visual Basic® 推論資料型別,而沒有在程式碼中指定呼叫 Enumerable 類別之擴充方法時的傳回值型別。另外,範例需要用到 SQL Express 中安裝的 Northwind 範例資料庫。如果您要執行範例,則需要仿造這樣的環境,不然就要修改範例應用程式。
深究 Enumerable
在您建立的每一個 LINQ 查詢中,Enumerable 類別扮演很重要的角色。例如,假設您的專案中有 Product 類別和 Product 執行個體的泛型清單 (就像這個範例專案一樣)。您已從範例中建立 SimpleDataContext 類別的執行個體,來將資料填入 Products 清單,且您寫了一個像這樣的查詢:
|
Dim db As New SimpleDataContext()
Dim query = _
From prod In db.Products _
Where prod.CategoryID = 1 _
Order By prod.ProductName _
Select prod.ProductName
|
詳細研究後,您會發現 Visual Basic 編譯器一定會在幕後將 SQL 式的語法轉換成某型別之執行個體上的一組方法呼叫。確實如此,在本範例中,SimpleDataContext 類別的 Products 屬性就是 List(Of Product) 型別,而您可能已猜到,List(Of T) 型別會實作 IEnumerable(Of T) 介面。編譯器會將 LINQ 關鍵字轉換成 Enumerable 型別上的相對擴充方法呼叫。具體地說,您可以將前述 LINQ 查詢改寫成這樣,一樣可建立一組完全相同的指令:
|
Dim query = _
db.Products _
.Where(Function(prod) CBool(prod.CategoryID = 1)) _
.OrderBy(Function(prod) prod.ProductName) _
.Select(Function(prod) prod.ProductName)
|
如果您在 LINQ 查詢中加上 Visual Basic 關鍵字來檢查編譯器所建立的中繼語言 (IL),您會看到編譯器已將 Visual Basic 關鍵字轉換成 Enumerable 類別之相對方法的呼叫。這樣做可以擴充 System.Collections.Generic.List(Of T) 類別的行為 (事實上,這很容易判斷,只要查閱此特定類別之成員的說明,就能找到擴充方法的清單,其中大部分都是由 Enumerable 類別所定義)。
這對您究竟有何意義?因為 Enumerable 類別的擴充方法可以處理其他許多類別,包括 Array 和 List,所以 Enumerable 類別的方法不僅可以用來建立 LINQ 查詢,也能用來操控陣列和其他資料結構的行為。
只要查閱 Enumerable 類別的擴充方法說明,就可以發現每一個成員都是共用 (Shared) 性質。因此,呼叫方法時可以有兩種選擇。第一種是把它當做宣告型別的方法來呼叫,並在其中傳入擴充類別的執行個體 (假設 results 變數是 Count 方法所能處理的型別),如下所示:
|
Dim itemCount = _
Enumerable.Count(results, _
Function(prod) prod.CategoryID = 3)
|
您也可以將 Count 當做集合本身的執行個體方法來呼叫。如果以這種方式來呼叫擴充方法,就不需要提供第一個參數:
|
Dim itemCount = _
results.Count(Function(prod) _
prod.CategoryID = 3)
|
在將關鍵字轉換成擴充方法呼叫時,Visual Basic 和 C# 各有不同的處理方式。Visual Basic 可讓您在 LINQ 查詢中使用的關鍵字比 C# 更多。例如,您可以在 Visual Basic 中使用 Take 關鍵字來建立 LINQ 查詢,如下所示:
|
Dim db As New SimpleDataContext()
Dim query = _
From prod In db.Products _
Where prod.CategoryID = 1 _
Order By prod.ProductName _
Take 10 _
Select prod.ProductName
|
您也可以使用 Enumerable 類別所提供的 Take 擴充方法,如下所示:
|
Dim query = _
(From prod In db.Products _
Where prod.CategoryID = 1 _
Order By prod.ProductName _
Select prod.ProductName).Take(10)
|
因為 C# 沒有提供 Take 關鍵字,所以您別無他法,只能明確地呼叫 Take 方法。[圖 1] 列出 Visual Basic LINQ 查詢關鍵字與 Enumerable 類別之相對方法之間的對應關係。

圖 1 LINQ 關鍵字與 Enumerable 方法的對應關係
| Visual Basic LINQ 查詢關鍵字 |
Enumerable 方法 |
| Aggregate … Into … All |
All |
| Aggregate … Into … Any |
Any |
| Aggregate … Into … Average |
Average |
| Aggregate … Into … Count |
Count |
| Aggregate … Into … LongCount |
LongCount |
| Aggregate … Into … Max |
Max |
| Aggregate … Into … Min |
Min |
| Aggregate … Into … Sum |
Sum |
| Distinct |
Distinct |
| Group By |
GroupBy |
| Group Join |
GroupJoin |
| Order By |
OrderBy |
| Order By … Descending |
OrderByDescending |
| Order By (使用多個欄位) |
ThenBy |
| Order By … Descending (使用多個欄位) |
ThenByDescending |
| Select |
Select |
| Skip |
Skip |
| Take |
Take |
| Take While |
TakeWhile |
| Where |
Where |
建立序列
Enumerable 類別有提供幾個 Shared 方法,但這些不是擴充方法,只是為了讓您用來建立新的序列。首先,我將討論這些簡單的方法。
Enumerable.Range 方法可以建立新的整數循序清單。您需要指定起始值和清單的項目數目。這個方法會傳回 IEnumerable(Of Integer) 序列。您可以利用這個方法來解決本文一開始的問題 (也就是,在兩個端點之間擷取一份隨機排序的數字清單)。
下列程式碼會使用 Range 方法來填入清單,接著呼叫 Enumerable.OrderBy 方法加上簡單 Lambda 運算式,以隨機順序來排列序列:
|
' From RangeDemo in the sample:
Dim rnd As New System.Random
Dim items = Enumerable.Range(1, 10)
Dim randomList = _
items.OrderBy(Function() rnd.Next())
|
如果將範例程序執行多次,每次都會傳回不同的結果,不過,每一次執行程式碼時,應該都會看到類似下列的結果:
|
6, 8, 7, 1, 2, 9, 10, 4, 3, 5
|
Enumerable.Reverse 方法會以相反順序傳回輸入序列。下列程式碼可以將 Reverse 方法當做 Enumerable 類別的 Shared 方法來呼叫,另外也可以當做呼叫 Enumerable.Range 方法所傳回的清單之執行個體方法來呼叫:
|
'From ReverseDemo in the sample:
Dim items = Enumerable.Range(1, 10)
Dim reversed = items.Reverse()
reversed = Enumerable.Reverse(items)
|
無論您如何呼叫 Reverse 方法,最後一定會得到下列清單:
|
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
|
Enumerable.Repeat 方法會建立一份清單,內含您提供的值,而且此值會以您指定的次數重複出現:
|
'From RepeatDemo in the sample:
Dim repeated = Enumerable.Repeat("Hello", 5)
|
呼叫上述程式碼後,repeated 變數就會包含下列項目:
|
Hello, Hello, Hello, Hello, Hello
|
Enumerable.Empty 方法可用來建立空的 IEnumerable(Of T),其中只含指定的資料型別。透過這個方法,您可以建立集合的空執行個體,再視需要來加入項目。只要將集合初始化成空的集合,就不用擔心參考是否為 Nothing:
|
'From EmptyDemo in the sample:
Dim emptyList = _
Enumerable.Empty(Of Customer)()
|
選取序列
Enumerable 類別提供兩個不同的擴充方法,可讓您選取元素序列,同時將輸入序列中的元素投射到輸出序列。Enumerable.Select 方法只是將每一個元素投射成新的格式。下列範例會從 Northwind Customers 資料表取得一個內含美國客戶的序列,接著將結果投射到另一個序列,此序列含有連絡人姓名的編號清單:
|
' From SelectDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
From cust In db.Customers _
Where cust.Country = "USA"
Dim selectResults = results.Select( _
Function(cust, index) _
String.Format("{0}. {1}", _
index + 1, cust.ContactName))
|
請注意,Select 方法有多個多載版本。執行這段程式碼後,selectResults 就會包含類似下列的值清單:
|
1. Howard Snyder
2. Yoshi Latimer
3. John Steel
4. Jaime Yorres
5. Fran Wilson
6. Rene Phillips
7. Paula Wilson
8. Jose Pavarotti
9. Art Braunschweiger
10. Liz Nixon
11. Liu Wong
12. Helvetius Nagy
13. Karl Jablonski
|
Enumerable.SelectMany 方法可接受項目集合 (其中每一個項目又可以是另外一個項目集合),然後將二維輸入摺疊成一維輸出序列。假設您需要取得一份分類清單,每一個分類都內含產品,最後要得到一份產品清單加上對應的分類資訊,在此情況下,這個方法就可派上用場。下列範例會擷取一份分類清單 (分類中的產品必須少於七項),然後將結果投射成單一清單,其中包含每一項產品的相關資訊及分類資訊:
|
' From SelectManyDemo in the sample:
Dim db As New SimpleDataContext
Dim categories = _
From cat In db.Categories Where cat.Products.Count < 7
Dim manyResults As IEnumerable(Of String) = _
categories.SelectMany(Function(cat, index) _
cat.Products.Select( _
Function(prod As Product) String.Format("{0}. Category {1}: {2}", _
index, prod.CategoryID, prod.ProductName)))
|
在此案例中,傳遞給 SelectMany 方法的 Lambda 運算式會接受特定分類的參考,並逐一檢查每一個分類。SelectMany 方法的這個多載,會將分類的索引及分類傳遞給 Lambda 運算式。取得分類及其索引後,Lambda 運算式就會在分類的 Products 屬性上呼叫 Select 方法,這樣就能處理分類中的每一項產品。範例會傳回含有下列字串清單的序列。呼叫 SelectMany 會將原始的分類/產品階層架構,簡化成單純的值清單:
|
0. Category 6: Mishi Kobe Niku
0. Category 6: Alice Mutton
0. Category 6: Thüringer Rostbratwurst
0. Category 6: Perth Pasties
0. Category 6: Tourtière
0. Category 6: Pâté chinois
1. Category 7: Uncle Bob's Organic Dried Pears
1. Category 7: Tofu
1. Category 7: Rössle Sauerkraut
1. Category 7: Manjimup Dried Apples
1. Category 7: Longlife Tofu
|
擷取單一元素
您有時候會需要擷取和處理序列中的單一元素。Enumerable 類別有提供幾個方法,可讓您從序列的內容中篩選出單一元素。Enumerable.First 和 Enumerable.Last 方法分別可傳回序列的第一個元素和最後一個元素。Enumerable.Single 方法會根據指定單一元素的函式或 Lambda 運算式,而傳回一個元素。Enumerable.ElementAt 方法會從序列內傳回特定索引上的單一元素。
如果所要求的元素不存在序列中,這些方法都會擲回例外狀況。以 Single 方法而言,如果限制導致未傳回元素或傳回多個元素,就會擲回例外狀況。這些方法也都可讓您指定 Lambda 運算式或函式,來限制輸入資料。在任何情況下,序列最後一定只能包含單一元素。
Enumerable 類別也提供類似的方法,能夠盡可能地傳回單一元素,如果不可能則會傳回序列型別的預設成員。這些方法包括 Enumerable.FirstOrDefault、Enumerable.LastOrDefault、Enumerable.SingleOrDefault 及 Enumerable.ElementAtOrDefault。[圖 2] 示範這些方法的行為。執行這個範例就會將下列資料放入輸出資料流:
|
ALFKI: Maria Anders
ALFKI: Maria Anders
Last in USA: Karl Jablonski
Sequence contains no elements
Specified argument was out of the range of valid values.
Parameter name: index
CustomerID = XXXXX is Nothing: True
Customer 2 in USA is Nothing: False
Customer 200 in USA is Nothing: True
|

圖 2 使用 Enumerable 方法擷取元素
|
' From SingleElementDemo in the sample:
Dim oneCustomer As Customer
Dim sw As New StringWriter
Dim db As New SimpleDataContext
Dim query0 = From cust In db.Customers
Dim query1 = _
From cust In db.Customers _
Where cust.CustomerID = "ALFKI"
Dim query2 = _
From cust In db.Customers _
Where cust.Country = "USA"
Dim query3 = _
From cust In db.Customers _
Where cust.CustomerID = "XXXXX"
' This works fine, finds the customer whose CustomerID is ALFKI:
oneCustomer = query1.Single
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)
' You can supply an expression to filter, when calling
' the First, Last, Single, and so on, methods:
oneCustomer = query0.Single( _
Function(cust) cust.CustomerID = "ALFKI")
sw.WriteLine("ALFKI: {0}", oneCustomer.ContactName)
' This works fine, finds the last customer in the USA:
oneCustomer = query2.Last
sw.WriteLine("Last in USA: {0}", oneCustomer.ContactName)
' These two raise exceptions:
Try
' There's no customer use customer ID is "XXXXX":
oneCustomer = query3.Single
Catch ex As Exception
sw.WriteLine(ex.Message)
End Try
Try
' There aren't 200 customers in the USA:
oneCustomer = query2.ElementAt(200)
Catch ex As Exception
sw.WriteLine(ex.Message)
End Try
' These don't raise exceptions, but return default values
' if they fail:
oneCustomer = query3.SingleOrDefault
sw.WriteLine("CustomerID = XXXXX is Nothing: {0}", _
oneCustomer Is Nothing)
' Because there are at least two customers in the USA,
' method returns a valid, single customer:
oneCustomer = query2.ElementAtOrDefault(2)
sw.WriteLine("Customer 2 in USA is Nothing: {0}", _
oneCustomer Is Nothing)
oneCustomer = query2.ElementAtOrDefault(200)
sw.WriteLine("Customer 200 in USA is Nothing: {0}", _
oneCustomer Is Nothing)
|
篩選序列
Enumerable.Where、Enumerable.Distinct 或 Enumerable.OfType 方法,都可用來篩選現有序列的內容,然後將原始資料的子集傳回到輸出序列中。OfType 方法可根據特定型別來篩選輸入序列。假設您只想對表單上的一個特定的控制項型別執行操作。您可以使用 OfType 方法來限制表單所呈現的控制項集合,並逐一查看相關的控制項子集。
下列範例會在泛型清單中填入資料,並擷取一份整數輸出清單,接著再擷取一份字串清單:
|
' From OfTypeDemo in the sample:
Dim items As New List(Of Object)
items.Add("January")
items.Add(0)
items.Add("Monday")
items.Add(3)
items.Add(5)
items.Add("September")
Dim numbers = items.OfType(Of Integer)()
Dim strings = items.OfType(Of String)()
|
執行上述程式碼後,這兩份輸出清單就會包含下列資料:
|
0, 3, 5
January, Monday, September
|
Enumerable.Where 方法可讓您指定條件來篩選輸入序列。第二個多載版本可以存取集合中每一個項目的索引,所以也能讓您根據索引來篩選。下列範例會使用 Where 與 Select 方法,從 C:\Windows 資料夾中擷取小於 100 個位元組的檔案序列,一開始先篩選,接著再投射結果:
|
' From WhereDemo in the sample:
Dim files As IEnumerable(Of FileInfo) = _
New DirectoryInfo("C:\Windows").GetFiles()
Dim fileResults = _
files _
.Where(Function(file) file.Length < 100) _
.Select(Function(file) _
String.Format("{0} ({1})", file.Name, file.Length))
|
由於陣列也是實作 IEnumerable 介面,所以您可以使用 Where 方法來篩選陣列內容,就像篩選其他任何集合一樣。在此案例中,程式碼會篩選 GetFiles 方法所傳回的 FileInfo 物件陣列。您也可以看到範例中接連地呼叫 Where 方法及 Select 方法。在我的電腦上,這個範例傳回下列結果:
|
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
S86D5A060.tmp (48)
setuperr.log (0)
|
您可以使用 Where 方法的第二個多載版本,這樣就能存取每一個所處理輸入項目的索引。例如,下列 Where 方法呼叫會傳回小於 100 個位元組且出現在前 20 個輸入檔案內的檔案:
|
' From WhereDemo in the sample:
fileResults = _
files _
.Where(Function(file, index) _
(file.Length < 100) And (index < 20)) _
.Select(Function(file) _
String.Format("{0} ({1})", file.Name, file.Length))
|
在此案例中,我的電腦上傳回下列結果 (缺少的檔案表示它們已超出前 20 個處理的檔案):
|
Addrfixr.ini (62)
bthservsdp.dat (12)
iltwain.ini (36)
|
Enumerable.Distinct 方法可讓您篩選清單,使輸出序列只包含輸入清單中的唯一項目。針對簡單的值 (字串、數字...等等),這個程序就很簡單。
如果是複雜物件的清單,執行階段引擎該如何進行比較,以判斷兩個物件是否重複?它不能只比較執行個體變數,因為事實上這只會比較物件的記憶體位址而已。相反地,對於複雜的物件,您必須提供比較子,這個比較子是一個實作 IEqualityComparer(Of T) 之類別的執行個體,以進行比較 (Enumerable 類別的其他幾個方法也有同樣的問題,本專欄稍後會再提及)。
假設您有一個包含客戶資訊的序列,但您想要有一份客戶來自什麼國家的唯一清單。如果您有一份單純的國家清單,則可以利用預設比較子來比較字串。在此案例中,您有一份客戶清單 (沒錯,您可以使用 Select 方法來投射清單,這樣就會只有一份國家清單,但在使用清單來做其他運算時,卻會失去其餘所有資料)。
範例專案包含 CustomerCountryComparer 類別,這個類別會實作 IEqualityComparer(Of Customer) 介面,如 [圖 3] 所示。範例程式碼會以兩種不同的做法,來示範如何使用 Distinct 方法。

圖 3 IEqualityComparer
|
Public Class CustomerCountryComparer
Implements IEqualityComparer(Of Customer)
Public Function Equals1( _
ByVal x As Customer, ByVal y As Customer) As Boolean _
Implements IEqualityComparer(Of Customer).Equals
Return x.Country.Equals(y.Country)
End Function
Public Function GetHashCode1( _
ByVal obj As Customer) As Integer _
Implements IEqualityComparer(Of Customer).GetHashCode
Return obj.Country.GetHashCode
End Function
End Class
|
第一種做法一開始會先將客戶清單投射到國家名稱清單,接著使用預設的字串比較子來縮減清單,使清單變成唯一的。第二種做法會使用特定的 CustomerCountryComparer 類別來進行比較,同時建立唯一清單,接著就將產生的客戶唯一清單投射成一份字串清單:
|
' From DistinctDemo in the sample:
Dim db As New SimpleDataContext
Dim countries As IEnumerable(Of String) = _
db.Customers _
.Select(Function(cust) cust.Country) _
.Distinct()
countries = customers _
.Distinct(New CustomerCountryComparer) _
.Select(Function(cust) cust.Country)
|
您可能需要小心處理空的序列,且 Enumerable.DefaultIfEmpty 方法可能會傳回整個輸入序列,或是會傳回預設值的執行個體 (如果序列是空的)。使用這個方法可以保證至少會傳回序列型別的單一項目。
[圖 4] 的程序示範 DefaultIfEmpty 方法的一種用法。這段程式碼會在 noCustomers 變數中填入空的清單 (因為沒有任何客戶的國家是 "XXX"),接著會呼叫 DefaultIfEmpty 方法來建立 noCustomers1 清單。執行範例程式碼會產生下列輸出:
|
noCustomers contains 0 element(s). noCustomers1 contains 1 element(s).
XXXXX
|

圖 4 使用 DefaultIfEmpty
|
' From DefaultIfEmptyDemo in the sample:
Dim db As New SimpleDataContext
Dim sw As New StringWriter
Dim noCustomers = _
From cust In db.Customers Where cust.Country = "XXX"
Dim noCustomers1 = _
noCustomers.DefaultIfEmpty()
Dim results = _
String.Format( _
"noCustomers contains {0} element(s). " & _
"noCustomers1 contains {1} element(s).", _
noCustomers.Count, noCustomers1.Count)
sw.WriteLine(results)
' You can specify the exact value to use if the
' collection is empty:
noCustomers1 = noCustomers.DefaultIfEmpty( _
New Customer With {.CustomerID = "XXXXX"})
For Each cust In noCustomers1
sw.WriteLine(cust.CustomerID)
Next
|
noCustomers1 包含一個元素 (一個未填入任何資訊的 Customer)。您可以呼叫 DefaultIfEmpty 方法來提供單一項目的預設值,就像第二個方法呼叫一樣。
排列序列
您可以使用 Enumerable.OrderBy、Enumerable.ThenBy、Enumerable.OrderByDescending 及 Enumerable.ThenByDescending 來指定初始排列順序,然後再指定一個或多個次要排列順序 (遞增或遞減順序)。這個範例一開始會先取得客戶序列,接著使用上述幾個方法來排列:
|
' From OrderByDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
From cust In db.Customers _
Where cust.ContactTitle = "Owner"
' Sort first by country, then by city descending, then by name:
Dim results = _
customers _
.OrderBy(Function(cust) cust.Country) _
.ThenByDescending(Function(cust As Customer) cust.City) _
.ThenBy(Function(cust As Customer) cust.ContactName) _
.Select(Function(cust) String.Format("({0}, {1}) {2}", _
cust.Country, cust.City, cust.ContactName))
|
請注意,在 ThenBy 和 ThenByDescending 方法呼叫中,Visual Basic 沒有規定您一定要指定 Lambda 運算式參數的資料型別,只是如果不指定的話,IntelliSense® 就無法發揮功能來協助您。只有第一個排列方法才能推斷 Lambda 運算式參數的資料型別。說也奇怪,Select 方法竟然可以正確地推斷型別,甚至在一長串的方法中也沒有問題。範例會先依國家來排序職稱是 "Owner" 的客戶,接著依城市來遞減排序,最後再依每一個堿市內的連絡人名稱來排序:
|
(Denmark, Kobenhavn) Jytte Petersen
(France, Paris) Marie Bertrand
(France, Nantes) Janine Labrune
(France, Marseille) Laurence Lebihan
(Germany, Köln) Henriette Pfalzheim
(Mexico, México D.F.) Ana Trujillo
(Mexico, México D.F.) Antonio Moreno
(Mexico, México D.F.) Miguel Angel Paolino
(Norway, Stavern) Jonas Bergulfsen
(Poland, Warszawa) Zbyszek Piestrzeniewicz
...
|
驗證序列
某些應用程式會要求您驗證序列是否符合特定的條件。有任何元素符合準則嗎?所有元素都符合準則嗎?序列內是否有出現特定的項目?兩個序列相等嗎?Enumerable 類別提供的方法,可以提供以上所有資訊。
若要判斷序列中究竟有沒有包含元素,請直接呼叫 Enumerable.Any 方法且不要傳入任何參數。若要提供條件,並判斷序列中是否有任何元素符合條件,請傳遞函式給方法。如果有輸入序列的元素存在,或符合指定的條件,這個方法就會傳回 True (請參閱 [圖 5])。

圖 5 尋找序列
|
' From AnyDemo in sample:
Dim db As New SimpleDataContext
Dim results = _
From product In db.Products _
Where product.CategoryID = 1
' Determine if a list has any elements:
Dim anyElements = results.Any()
' Call as extension method or shared method
' of IEnumerable:
Dim matchingElements = _
results.Any(Function(prod) prod.ProductName.StartsWith("M"))
matchingElements = _
Enumerable.Any(results, _
Function(prod As Product) prod.ProductName.StartsWith("M"))
|
若要判斷序列的所有成員是否全部都符合準則,請呼叫 Enumerable.All 方法,並提供函式來指定條件。這個範例會判斷結果集合中,是否所有元素的 UnitsInStock 值都大於 5:
|
' From AllDemo in sample:
' Is it true that all products have more than 5 units in stock?
Dim db As New SimpleDataContext
Dim results = _
From product In db.Products _
Where product.CategoryID = 1
Dim allElements = _
results.All(Function(prod) CBool(prod.UnitsInStock > 5))
|
若要判斷序列是否包含特定元素,請呼叫 Enumerable.Contains。如果您要搜尋簡單的值,像是字串或整數,您可以使用預設比較子,且只需要提供您要搜尋的值即可。如果您要判斷一個較複雜的物件是否存在某一序列內,則必須提供一個實作 IEqualityComparer(Of T) 之類別的執行個體。
[圖 6] 的範例程式碼示範這兩種呼叫 Enumerable.Contains 的做法。第一個範例只會判斷清單內是否有出現數字 5,第二個範例則會判斷產品內是否有出現對應於 Chai 的元素 (這個範例所使用的 ProductComparer 類別,就是本專欄稍早提到的類別)。

圖 6 在序列中尋找項目
|
' From ContainsDemo in the sample:
' Simple check to see if list contains a value:
Dim numbers = Enumerable.Range(1, 10)
Dim contains5 = numbers.Contains(5)
' More complex objects require more complex comparison:
Dim db As New SimpleDataContext
Dim results = _
From product In db.Products _
Where product.CategoryID = 1
Dim item As New Product _
With {.ProductID = 1, .ProductName = "Chai"}
Dim containsChai = results.Contains(item, New ProductComparer())
|
若要判斷兩個序列是否相等,請呼叫 Enumerable.SequenceEqual 方法。就像 Enumerable.Contains 方法一樣,您可以使用預設比較子來比較兩個包含簡單值的序列,也可以使用自訂的比較子來比較兩個包含複雜物件的序列。
如果兩個序列不含相同的型別或長度不同,則比較會立即失敗。如果包含相同型別且長度相同,則 Enumerable.SequenceEqual 方法可使用指定的比較子來比較每一個項目。[圖 7] 示範兩種呼叫 Enumerable.SequenceEqual 方法的做法。在這兩個案例中,傳回值都是 False。

圖 7 比較序列
|
' From SequenceEqualDemo in sample:
Dim rnd As New Random
Dim start = rnd.Next
Dim count = 10
Dim s1 = Enumerable.Range(start, count)
' Choose a different random starting point:
start = rnd.Next
Dim s2 = Enumerable.Range(start, count)
Dim sequencesEqual = s1.SequenceEqual(s2)
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)
' What if there's no default comparer? Must use your own:
Dim products1 = _
From prod In db.Products _
Where prod.CategoryID = 1
Dim products2 = _
From prod In products1 _
Where prod.UnitPrice > 5
sequencesEqual = _
products1.SequenceEqual(products2, New ProductComparer())
sw.WriteLine("sequencesEqual = {0}", sequencesEqual)
|