LINQ之路16:LINQ Operators之集合運算符、Zip操作符、轉換方法、生成器方法
本篇將是關于LINQ Operators的最后一篇,包括:集合運算符(Set Operators)、Zip操作符、轉換方法(Conversion Methods)、生成器方法(Generation Methods)。集合運算符用語對兩個sequence進行操作;Zip運算符同步遍歷兩個sequence(像一個拉鏈一樣),返回的sequence基于在每一個元素對上應用lambda表達式;轉換方法用來將實現了IEnumerable<T>的sequence轉換到其他類型的集合,或從其他類型的集合轉換到sequence;生成器方法/Generation Methods用來創建簡單的本地sequence。
集合運算符/Set Operators
IEnumerable<TSource>, IEnumerable<TSource>→IEnumerable<TSource>
|
Operator |
說明 |
SQL語義 |
|
Concat |
連接兩個sequences的所有元素 |
UNION ALL |
|
Union |
連接兩個sequences的所有元素,但去除重復的元素 |
UNION |
|
Intersect |
返回在兩個sequence中都存在的元素 |
WHERE ... IN (...) |
|
Except |
返回存在于第一個sequence而不存在于第二個sequence中的元素 |
EXCEPT or WHERE ... NOT IN (...) |
Concat和Union
Concat返回第一個sequence中的所有元素,后接第二個sequence中的所有元素,即連接兩個sequence。Union完成相同的工作,但是會去除重復的元素。
int[] seq1 = { 1, 2, 3 }, 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 }
如果兩個sequence的類型不同但是他們的元素共享同一個基類型時,我們通常明確指定類型參數。比如,在使用反射API時,方法和屬性分別用MethodInfo和PropertyInfo類型表示,他們的共同基類是 MemberInfo。我們可以在調用Concat時明確指定基類型來連接方法和屬性:
MethodInfo[] methods = typeof(string).GetMethods();
PropertyInfo[] props = typeof(string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo>(props);
下面的示例中,我們在連接之前先對Methods進行了過濾:
var methods = typeof(string).GetMethods().Where(m => !m.IsSpecialName);
var props = typeof(string).GetProperties();
var both = methods.Concat<MemberInfo>(props);
有意思的是,這個示例可以在C# 4.0中編譯但不能在C# 3.0中編譯,因為它依賴于接口類型參數協變:methods的類型是IEnumerable<MethodInfo>,在其轉換為IEnumerable<MemberInfo>時需要C# 4.0提供的協變功能。這個例子很好的說明了協變如何讓事情按我們期望的方式來工作。
Intersect和Except
Intersect返回兩個sequence中都存在的元素;Except返回存在于第一個sequence而不存在于第二個sequence中的元素:
int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };
IEnumerable<int>
commonality = seq1.Intersect(seq2), // { 3 }
difference1 = seq1.Except(seq2), // { 1, 2 }
difference2 = seq2.Except(seq1); // { 4, 5 }
Enumerable.Except的內部工作方式是:把第一個sequence中的所有元素裝載到一個dictionary中,然后從這個dictionary中移除所有存在于第二個sequence中的元素。它等價于SQL中的NOT EXISTS或NOT IN子查詢:
SELECT number FROM numbers1Table
WHERE number NOT IN (SELECT number FROM numbers2Table)
The Zip Operator
IEnumerable<TFirst>, IEnumerable<TSecond>→IEnumerable<TResult>
Zip運算符在Framework 4.0被添加進來。它同步遍歷兩個sequence(像一個拉鏈一樣),返回的sequence基于在每一個元素對上應用lambda表達式。如下例:
int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };
IEnumerable<string> zip = numbers.Zip (words, (n, w) => n + "=" + w);
// 產生的sequence包含如下元素
// 3=three
// 5=five
// 7=seven
任何輸入sequence中額外的元素(不能組成元素對)會被忽略。Zip運算符在查詢數據庫時不被支持。
轉換方法/Conversion Methods
LINQ主要用來處理sequences:即IEnumerable<T>類型的集合。 轉換方法用來將sequence轉換到其他類型的集合,或從其他類型的集合轉換到sequence。
|
方法 |
說明 |
|
OfType |
把IEnumerable轉換到IEnumerable<T>,丟棄錯誤的類型元素 |
|
Cast |
把IEnumerable轉換到IEnumerable<T>,如果存在錯誤的類型元素則拋出異常 |
|
ToArray |
把IEnumerable<T>轉換到T[] |
|
ToList |
把IEnumerable<T>轉換到List<T> |
|
ToDictionary |
把IEnumerable<T>轉換到Dictionary<TKey,TValue> |
|
ToLookup |
把IEnumerable<T>轉換到ILookup<TKey,TElement> |
|
AsEnumerable |
向下轉換到IEnumerable<T> |
|
AsQueryable |
轉換到IQueryable<T> |
OfType和Cast
OfType和Cast接收一個非泛型的IEnumerable集合,返回一個泛型的IEnumerable<T>,這樣我們就可以對其進行查詢了。
ArrayList classicList = new ArrayList(); // in System.Collections
classicList.AddRange(new int[] { 3, 4, 5 });
IEnumerable<int> sequence1 = classicList.Cast<int>();
Cast和OfType的不同行為表現在當遇到一個類型不兼容的輸入元素時,Cast拋出一個異常,而OfType忽略不兼容的元素。繼續上一個示例:
DateTime offender = DateTime.Now;
classicList.Add(offender);
IEnumerable<int>
sequence2 = classicList.OfType<int>(), // OK - ignores offending DateTime
sequence3 = classicList.Cast<int>(); // Throws exception
判斷元素類型是否兼容的規則與C#的is操作符一致,我們可以通過OfType的內部實現來進行驗證:
public static IEnumerable<TSource> OfType<TSource>(IEnumerable source)
{
foreach (object element in source)
if (element is TSource)
yield return (TSource)element;
}
Cast具有相同的實現,除了它里面省略了類型兼容性的檢測:
public static IEnumerable<TSource> Cast<TSource>(IEnumerable source)
{
foreach (object element in source)
yield return (TSource)element;
}
這些實現的一個結果是我們無法使用Cast來進行數值或定制的轉換(對這些我們必須使用Select運算符)。換句話說,Cast不具備C#中cast操作符的靈活性:
int i = 3;
long l = i; // 隱式數值轉換 int->long
int i2 = (int)l; // 顯式數值轉換 long->int
現在我們再來看看OfType和Cast轉換int sequence到long sequence的行為:
int[] integers = { 1, 2, 3 };
IEnumerable<long> test1 = integers.OfType<long>();
IEnumerable<long> test2 = integers.Cast<long>();
當我們遍歷結果時,test1產生0個元素的sequence,而test2會拋出異常。我們再次審視OfType的實現,原因顯而易見。用int代入TSource之后,下面的表達式(element is long)返回false,因為int和long之間并沒有繼承關系。
而test2拋出異常的原因就沒那么明顯了,注意Cast的實現中元素的類型為object。當TSource是一個值類型時, CLR認為這是一個拆箱轉換。而拆箱操作要求精確的類型匹配,所以當TSource是一個int時,object-to-long拆箱會失敗并拋出異常。可以通過下面的示例進行驗證:
int value = 123;
object element = value;
long result = (long)element; // exception
正如我們前面的建議,此問題的解決方案是使用Select:
IEnumerable<long> castLong = integers.Select(s => (long)s);
當我們需要把一個泛型的輸入sequence中的元素向下轉換(轉為更具體的類型)時,OfType和Cast也會非常有用。比如,如果我們有一個IEnumerable<Fruit>的輸入sequence,OfType<Apple>僅返回apples,這在我們稍后會講到的LINQ to XML中會特別有效。
Cast有對應的查詢表達式語法支持:只需在范圍變量之前指定一個類型即可:
var query =
from TreeNode node in myTreeView.Nodes
...
ToArray, ToList, ToDictionary, 和ToLookup
ToArray和ToList分別把結果輸出到一個數組或泛型列表。這些運算符會強制對輸入sequence進行立即遍歷(除非間接通過子查詢或表達式樹)。關于他們的示例請參考:LINQ之路 6:延遲執行(Deferred Execution)
ToDictionary和ToLookup接受如下參數:
|
參數 |
類型 |
|
輸入sequence / Input sequence |
IEnumerable<TSource> |
|
鍵選擇器 / Key selector |
TSource => TKey |
|
元素選擇器 / Element selector(optional) |
TSource => TElement |
|
比較器 / Comparer (optional) |
IEqualityComparer<TKey> |
ToDictionary也會強制sequence的立即執行,并把結果寫入一個泛型Dictionary。提供的鍵選擇器表達式必須為每個元素產生唯一的鍵值,否則將會拋出異常。而ToLookup允許多個元素使用同一個鍵值。對于lookups我們已經在LINQ之路13:LINQ Operators之連接(Joining)中有過詳細介紹。
AsEnumerable和AsQueryable
AsEnumerable把一個sequence向上轉換到IEnumerable<T>,強制編譯器把后來的查詢運算符綁定到Enumerable類中的方法,而不是Queryable類的方法。他們的示例請參考LINQ之路8:解釋查詢(Interpreted Queries)中的“組合使用解釋查詢和本地查詢”一節。
AsQueryable把一個sequence向下轉換到IQueryable<T>(如果它實現了該接口)。否則,它會實例化一個IQueryable<T>包裝器用于本地查詢之上。
生成器方法/Generation Methods
void→IEnumerable<TResult>
|
Operator |
說明 |
|
Empty |
創建一個空sequence |
|
Repeat |
創建一個含有重復elements的sequence |
|
Range |
創建一個包含指定整數的sequence |
Empty, Repeat, 和Range是Enumerable類的靜態方法,而不是擴展方法,用來創建簡單的本地sequence。
Empty
Empty創建一個空的sequence,它只需要一個類型參數:
foreach (string s in Enumerable.Empty<string>())
Console.Write(s); // <nothing>
通過配合使用??操作符,Empty可以完成DefaultIfEmpty相反的操作。比如,我們有一個交錯整形數組,現在我們想要把所有的整數放到一個簡單的平展列表中。下面的SelectMany查詢在遇到一個空的內部數組時會失敗:
int[][] numbers =
{
new int[] { 1, 2, 3 },
new int[] { 4, 5, 6 },
null // this null makes the query below fail.
};
IEnumerable<int> flat = numbers.SelectMany(innerArray => innerArray);
Empty搭配??就可以解決該問題:
IEnumerable<int> flat = numbers
.SelectMany(innerArray => innerArray ?? Enumerable.Empty<int>());
foreach (int i in flat)
Console.Write(i + ""); // 1 2 3 4 5 6
Range 和Repeat
Range和Repeat只能用來操作integers。Range接受一個起始索引和元素個數:
foreach (int i in Enumerable.Range(5, 5))
Console.Write(i + ""); // 5 6 7 8 9
Repeat接受重復的數值,和該數值重復的次數:
foreach (int i in Enumerable.Repeat(5, 3))
Console.Write(i + ""); // 5 5 5
通過六篇博客的篇幅,我們對于LINQ Operators的介紹終于告一段落了,我也長舒了一口氣。一方面希望給閱者最全面最詳細的介紹,另一方面又怕再不結束LINQ Operators,只怕閱者都會產生審美疲勞了,^_^!
關于LINQ,我們還有一塊沒有介紹,它就是LINQ to XML,我準備用3-4篇左右的篇幅來對它進行討論,等到LINQ to XML完成的那一天,LINQ之路系列博客也就大功告成了。希望廣大博友繼續給我支持,給我力量,謝謝。^_^

浙公網安備 33010602011771號