1、LINQ 函數
1.1、查詢結果過濾 :where()
Enumerable.Where()
是LINQ 中使用最多的函數,大多數都要針對集合對象進行過濾,因此Where()
在LINQ 的操作上處處可見,Where()
的主要任務是負責過濾集合中的數據:其原型如下:
1 public static IEnumerbale<TSouce> Where<TSource>(this IEnumerable<Tsource> source,Func<TSource,bool> predicate); 2 public static IEnumerable<TSource>where<TSource> (this IEnumerable<TSource> source,Func<TSource,int,bool> predicate);
Where()
的參數是用來過濾元素的條件,它要求條件必須傳回bool
,以確定此元素是否符合條件,或是由特定的元素開始算起(使用Func<TSource,int bool>
,中間的傳入參數代表該元素在集合中的索引值),例如要在一個數列集合中找出大於5的數字時:
1 List<int> list1=new List<int>(){6,4,2,7,9,0}; 2 3 list1.Where(c=>c>5);
或者
1 list1.Where(c=>c>=1).Where(c=>c<=5); 2 3 list1.Where(c=>c>=1&&c<=5);
Where()
的判斷標准是,只要判斷函數返回true
就成立,反之則取消。
1.2、選取數據: Select()、SelectMany()
通常在編寫LINQ 函數調用時較少用到選取數據的函數(因為函數調用會直接返回IEnumerable<T>
集合對象 ),但在編寫LINQ語句時十分常用,在語句中若編寫了select new
指令,它會被編譯器轉換成LINQ 的Select()
,Select()
的原型如下:
1 public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source,Func<TSource,TResult> selector); 2 public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source,Func<TSource,int,TResult> selector);
與Where()
類似,Select()
也可以按照元素所在的位置判斷處理,而Select()
所指定的處理式selector 必須傳回一個對象,這個對象可以是現有的類型,也可以是匿名的類型,既可以通過Select()
來重新組裝所需數據。例:
1 var query=db.OrderDetails.Where(o=>o.ID==12345).Select(o=>new{ ProductID=o.ProductID,Qty=o.Qty});
Select()
的另一個相似函數SelectMay()
則是處理有兩個集合對象來源的數據選取,其原型如下:
1 public static IEnumerable<TResult> SelectMany<TSource,TResult>(this IEnumerable<TSource> source,Func<TSource,IEnumberable<TResult>> selector); 2 public static IEnumerable<TResult> SelectMany<TSource,TResult>(this IEnumerable<TSource> source,Func<TSource,int,IEnumberable<TResult>> selector); 3 public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult>(this IEnumerable<TSource> source,Func<TSource,IEnumberable<TCollection>> collectionSelector,Func<TSource,TCollection,TResult> resultSelector); 4 public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult>(this IEnumerable<TSource> source,Func<TSource,int,IEnumberable<TCollection>> collectionSelector,Func<TSource,TCollection,TResult> resultSelector);
SelectMany()
在LINQ 函數調用上較難理解,但如果把它想象成數據庫
CROSS JOIN
,相對來說就容易懂了,例:
1 List<int> list1=new List<int>(){1,2,3,4,5,6}; 2 List<int> list2=new List<int>(){6,4,2,7,9,0}; 3 4 var query=list1.SelectMany(o=>list2); 5 foreach(var q in query) 6 Console.WriteLine("{0}",q);
輸出結果:
1 6424790642790642790642790642790642790
因為“642790”
輸出了 6次,list1 內的元素是6個,所以可以知道SelectMany()
會按照list1 內的元素個數調用它的selector,並組裝集合輸出。
1.3、群組數據:GroupBy()、ToLookup()
匯總數據是查詢機制的基本功能,而在匯總之前,必須要先將數據做群組化,才能進行統計,LINQ 的群組數據功能由Enumerable.GroupBy()
函數提供。
GroupBy()
會按照給定的key(keySelector)
以及內容(elementSelector
),產生群組后的結果(IGroup
接口對象或是由resultSelector
生成的結果對象),例:
1 List<int> sequence =new List<int>(){1,2,3,4,3,2,4,6,4,2,4}; 2 3 var group=sequence.GroupBy(o=>o); 4 foreach(var g in group) 5 { 6 Console.WrilteLine("{0} count:{1}",g.Key,g.Count()); 7 /*計算每個數出現的次數。 8 GroupBy 設置了使用數列本身值作為Key值,並且利用這個Key 分組產生分組的數據(IGrouping<TKey,TElement類型),再對分組的數據進行匯總。結果如下: 9 10 1 count: 1 11 2 count: 3 12 3 count: 2 13 4 count: 4 14 6 count: 1 15 16 */
若是想要在返回之前對分組后的元素做處理,可以傳入elementSelector
而若是要在元素處理后產生結果的話,則可以傳入resultSelector
,這樣返回的集合會是以resultSelector
返回的類型為主,而不是默認的IGroup
接口。
除了GroupBy()
能群組化數據外、另外一個具有群組化數據能力的是ToLookUp()
,它可以生成具有群組化特性的集合對象,由ILookup<TKey,TElement>
組成。
ToLookup(
)看起來和GroupBy()
有些類似,但是它會另外生成一個新的集合對象,這個集合對象由ILookup<TKey,TElement>
所組成,允許多個鍵值存在,且一個鍵值可包含許多關聯的實值。例:
1 var nameValuesGroup=new[] 2 { 3 new{name="Allen", value=65,group="A"}, 4 new{name="Abbey",value=120,group="B"}, 5 new{name="Sue",Value=200,group="A"} 6 }; 7 var lookupValues=namValuesGroup.ToLookup(c=>c.group); 8 foreach(var g in lookupValues) 9 { 10 Console.WriteLine("===Group: {0}===",g.Key); 11 foreach(var item in g) 12 { 13 Console.WriteLine("name:{0},value:{1}",item.name,item.value); 14 } 15 }
GroupBy()本身具有延遲執行的特性,而ToLookup()沒有。
1.4、聯接數據: Join() 與GroupJoin()
身為一個查詢機制,將兩個集合進行聯接(join)也是理所當然的,尤其是在進行數據的對比和匯總時,聯接機制顯得更重要。在LINQ 函數中,有 Enumerable.Join()
函數負責處理聯接,其原型如下:
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult>(this IEnumerable<TOuter> outer,IEnumerable<TInner> inner,Func<TOutput,TKey> outerKeySelector,Func<TInner,TKey> innerKEySelector,Func<TOuter,TInner,TResult> resultSelector)
由原型可看到它將原本的集合視為TOuter
,而將傳入的集合視為TInner
,兒還要決定由哪個屬性或成員當Key,最后由resultSelector
來輸出聯接的結果。例:
1 var query=from item1 in list1 2 join item2 in list2 on item1 equals item2 3 select item2; 4 var query3=list1.Join( 5 list2, 6 item1=>item1, 7 item2=>item2, 8 (item1,item2)=>item2 9 );
Enumerable<T>.Join()
使用的是INNER JOIN
的概念,當TInner.Key
和TOuter.Key
相同時,才會將元素輸出到resultSelector
作為參數。
目前常用的聯接模式,INNER JOIN
由 Enumerable<T>.Join()
實現,CROSS JOIN
由Enumerable<T>.SelectMany()
實現,還有一種JOIN 模式沒有考慮,LEFT OUTER JOIN
模式,要實現這個模式,必須要借助GroupJoin()
方法來實現。
GroupJoin
和 Join()
十分相似,不過它卻又Join()
和GroupBy()
兩者的功能,在Join()
的情況下,它會留下TInner
和TOuter
兩邊都有的值,但在GroupJoin()
,它會將TOuter
的值作為Key
,並依此來對TInner
做群組化后輸出,例:
1 var query4=from item1 in list1 2 join item2 in list2 on item1 equals item2 into g 3 from item in g.DefaultIfEmpty() 4 select new{ v=item1,c=item}; 5 var query5=list1.GroupJoin( 6 list2, 7 item1=>item1, 8 item2=>item2, 9 (item1,item2)=>new {v=item1,c=item2.Count()});
1.5、數據排序:OrderBy() 與ThenBy()
數據排序是在數據處理中常見的功能,在LINQ 內的排序主要是以OrderBy
函數為主,而為了支持連續條件的排序,可加上ThenBy
函數,以便處理多重條件排序的需求。基於LINQ的延遲查詢機制,排序也不是在一開始就進行的,而是在數據真的被訪問時才會進行排序。因此OrderBy()
在處理集合時,傳遞回來的是稱為IOrderedEnumerable<T>
接口的對象。
OrderBy
和ThenBy
還有一個相似的方法,差別只在於做反向排序。OrderByDescending
和ThenByDescending
。
觀察函數的原型,會發現OrderBy
傳入的是IEnumerable<T>
,但ThenBy
傳入的是IOrderedEnumerable
,所以一般在排序時先調用OrderBy
,再使用ThenBy
進行多重排序。假設一個集合有A和B兩個屬性,如果想要先為A排序再為B排序,則要使用OrderBy(A).ThenBy(B)
的方式來進行排序,OrderBy
和ThenBy
一次調用只能設置一個字段,在進行多重條件時,必須先調用OrderBy
,再按需求調用ThenBy
一次或多次。例:
1 var nameValues=new[] 2 { 3 new {name="Allen",value=64}, 4 new {name="abbey",value=120}, 5 new {name="slomng",value=330}, 6 new {name="george",value=213} 7 }; 8 //single sort 9 var sortedNames=nameValues.OrderBy(c=>c.name); 10 var sortedValues=nameValues.OrderBy(c=>c.value); 11 12 //multiply sort conditions 13 var sortedByNameValues=nameValues.OrderBy(c=>c.name).ThenBy(c=>c.value); 14 var sortedByValueNames=nameValues.OrderBy(c=>c.value).ThenBy(c=>c.name);
如果要設置多重排序條件,請務必使用OrderBy()
加上ThenBy()
的組合,若使用OrderBy +OrderBy
組合,會使得排序被執行兩次,最終的結果會是最后一個OrderBy
所產生的的結果。
1.6、獲取集合
LINQ 所處理的數據都由集合而來,因此將LINQ 執行的結果轉換成集合也很容易。LINQ本身支持四種不同的集合生成方式,包含生成數組的ToArray()
、生成列表的ToList
、生成字典集合的ToDictionary
以及生成Lookup<TKey,TElement>
類的ToLookup
。例:
1 var arrayOutput=nameValues.ToArray(); 2 var listOutput=nameValues.ToList(); 3 4 var dictOutput1=nameValues.ToDictionary(c=>c.name); 5 var dictOutput2=nameValues.ToDictionary(c=>c.name,c=>value);
1.7、划分並獲取集合
Skip()
、 SkipWhile()
、 Take()
、 TakeWhile()
。在數據庫查詢時,為了達到最佳的性能,在數據量大時要進行分頁處理(paging)。上面四個函數的功能就是在大集合內切出少量數據。
1 public static IEnumberable<TSource> Skip<TSource>( 2 this IEnumerable<TSource> source, 3 int count 4 ) 5 public static IEnumberable<TSource> SkipWhile<TSource>( 6 this IEnumerable<TSource> source, 7 Func<TSource,bool> predicate 8 ) 9 public static IEnumberable<TSource> SkipWhile<TSource>( 10 this IEnumerable<TSource> source, 11 Func<TSource,int ,bool> predicate 12 ) 13 public static IEnumberable<TSource> Take<TSource>( 14 this IEnumerable<TSource> source, 15 int count 16 ) 17 public static IEnumberable<TSource> TakeWhile<TSource>( 18 this IEnumerable<TSource> source, 19 Func<TSource,bool> predicate 20 ) 21 public static IEnumberable<TSource> TakeWhile<TSource>( 22 this IEnumerable<TSource> source, 23 Func<TSource,int ,bool> predicate 24 )
Skip()
用來在集合中跳躍,讓LINQ 核心直接將游標跳到指定的位置,而不用通過“巡航”來移動,在大型集合中可節省不少時間,而SkipWhile 也有相同作用,但多了判斷式,也就是跳過符合條件的元素,而不同的SkipWhile()
可用來決定要跳過符合條件的或是判斷跳過特定的索引值。
Take()
用來傳回集合中特定數量的元素,它會告知LINQ 核心直接返回它所指定的元素數量,很適合使用與分頁的功能。TakeWhile 則是和SkipWhile 類似都是多了條件判斷式,不過TakeWhile 在元素滿足條件時,就返回該元素或是符合特定的索引值條件時返回該元素。
1.8、訪問元素
IEnumerable<T>
本身就是集合對象,所以針對集合對象所需要的元素訪問也是必要的功能,LINQ里的元素訪問功能是判斷容器內是否含有元素等。
首先是獲取首尾的元素,分別由First()
以及Last()
兩個方法負責,它們還各有一個姐妹方法FirstOrDefault()
以及LastOrDefault()
前者若沒有第一個或最后一個元素時,會傳回null,而后者會傳回其類型的默認值(基本上就是default(T)的結果)。
FirstOrDefault()
以及 LastOrDefault()
都沒有提供默認的設置方式,因此若想要使用非default(T)
的默認值,要使用DefaultEmpty()
來設置。First()
和Last()
都能傳入判斷元素是否符合條件的參數,當條件判斷存在時,First 會從集合的前面開始掃描,並返回掃描到符合條件的第一個元素,Last 則是反過來從集合的尾端開始掃描,並返回掃描到符合條件的第一個元素。例:
1 var firstLastItems=new []{"zero","two","three","four","five"}; 2 string firstContainsO=firstLastItems.First(s=>s.Contains('o')); 3 string lastContainsO=firstLastItems.Last(s=>s.Contains('0'));
LINQ 內還有一個Single,他會在集合中只有一個元素時傳回該元素,但若集合是空的或是有兩個以上的元素時會調用例外處理,或是使用它的姐妹方法SingleOrDefault 傳回null值,實用性比fisrt和last 低。
LINQ 提供了ElementAt()
這個方法,可按照索引值訪問元素,他有個相似方法ElementAtOrDefault
作用和firstordefault/lastordefault
是相同的。當找不到元素時就返回默認值。例:
1 var firstLastItems=new []{"zero","two","three","four","five"}; 2 string itematThree=firstLastITems.ElementAt(2);
若要判斷集合內有沒有特定值,LINQ 提供了Contains, 可以判斷集合捏有沒有傳入的元素,但因為Contain 會判斷對象是否相等,所以它另外提供了一個可傳入IEqualityComparer<T>
的作為比較依據的重載(overload)方法,可用於自定義類對象的相等比較操作。
若要判斷集合內有沒有值,LINQ 提供了兩個方法,一個是Count()
, 另一個是Any()
,除了可以簡單判斷集合內有沒有值外,也可以傳入判斷條件來決定是否要列入計算。通常會習慣使用Count
來判斷集合內是否存在任何元素,為什么要多做一個Any
呢。其實是考慮到LINQ 可能的查詢對象會包含遠程數據庫,不一定只有本地的數據源。對於遠程的數據源,如果使用Count ,要花費較高的成本來讀取數據后進行計數在傳回,但若是使用Any()
,則遠程只要判斷符合條件的數據是否存在一筆即可,不需要完整計數,所以針對遠程數據源,使用Any 來判斷有無數據是較好的選擇。針對本地的集合 any 和count 幾乎沒有差異。
若要判斷集合內的元素是否全部符合特定條件時, 可以利用LINQ 的All(), 它可以按照傳入的條件來掃描所有元素,只有在所有元素都符合條件時,或是集合時空時才會返回true ,否則會返回false。
若要按照元素的類型進行篩選的話,除了使用Where 對每個元素做類型信息判斷外,LINQ 也提供了一個更簡便的方法 OfType<T>()
,它可以傳回集合內符合T所指定類型的信息,這個方法很適合用在集合內包含了已實現了許多接口的類對象。然后使用OfType<T>
按照接口類型進行篩選。
OfType<T>
還有一個類似方法Cast<T>
,功能與OfType <T>
相同,但Cast<T>
會試圖把集合內的元素類型轉換成T類型,若無法進行類型轉換時會調用InvalidCastException
例外處理。若使用OfType<T>
則不會引發例外處理。
1.9、聚合與匯總
聚合運算(aggregation)是集合數據處理的重要功能之一,基本的Max
,Min
,Sum
,Average
以及可自己制定聚合規則的Aggregate()
。
Aggregate 是可暫存每一步計算結果的方法,它允許程序員按照傳入的條件對每個集合內的元素進行計算,而在每次調用時,他都會將前一次的結果暫存起來,並作為下次計算的傳入參數。Aggregate 基本上做到三種工作,第一種是直接按照傳入的條件來處理累計運算;第二種是可在調用時傳入一個種子值(seed),這個種子值會在開始進行運算時作為基准使用,之后可按照第一次對種子值的運算方式開始做累計運算;第三種則是在傳回之前做最后的處理,例:
1 double myBalance=100.0; 2 3 int[] withdrawItems={20,10,40,50,10,70,30}; 4 5 double balance=withdrawItems.Aggregate(myBalance,(originbalance,nextWithdrawal)=>{ 6 Console.WriteLine("originbalance:{0},nextWithdrawak:{1}",originbalance,nextdrawal); 7 Console.WriteLine("Withdrawal status:{0}",(nextWithdrawal<=originbalance)?"OK":"FAILED"); 8 9 return ((nextWithdrawal<=originbalance)?(originbalance-nextWithdrawal):originbalance); 10 }); 11 Console.WriteLine("Ending balance:{0}:",balance);
若要對最終的存款值進行處理,即可使用第三個參數resultSelector,例:
1 var balanceStatus= 2 withdrawItems.Aggregate(myBalance,(originbalance,nextWithdrawal)=>{ 3 return((nextWithdrawal<=originbalance)?(originbalance-nextWithdrawal):originbalance); 4 5 }, 6 (finalbalance)=> 7 { 8 return (finalbalance>=1000)?"Normal":"Lower"; 9 });
2、標准的查詢操作符
2.1 篩選
例:找出贏得至少15場比賽的碧璽和奧地利賽車手。代碼如下:
1 var racers=from r in Formula1.GetChampions() 2 where r.Wins > 15 && (r.Country=="Brazil"||r.Country=="Austria") 3 select r; 4 5 foreach(var r in racers) 6 { 7 Console.WriteLine("{0:A}",r); 8 }
下面使用Where()
和 Select()
的代碼:
1 var racers=Formula1.GetChampions(). 2 Where(r=>r.Wins>15 && (r.Country=="Brazil" || r.Country=="Austria")). 3 Select(r=>r);
2.2 用索引篩選
不能使用LINQ 查詢的一個例子是Where
方法的重載。在Where
方法的重載中,可以傳遞第二個參數——索引。索引是篩選器返回每個結果的計數器。可以在表達式中使用這個索引, 執行基於索引的計算。下面的代碼由Where 擴展方法調用,它使用索引返回姓氏以“A”
開頭,索引為偶數的賽車手。
1 var racers=Formula1.GetChamptions(). 2 Where((r,index)=>r.LastName.StartsWith("A") && index % 2 !=0); 3 4 foreach(var r in racers) 5 { 6 Console.WriteLine("{0,A}",r); 7 } 8
2.3 類型篩選
為了進行基於類型的篩選,可以使用OfType
擴展方法。這里數組數據包含string 和 int 對象。 使用OfType
擴展方法,把string
類傳遞給泛型參數,就從集合中返回字符串。
1 object[] data={"ones",1,3,"fre","fdfs",333}; 2 var query=data.OfType<string>(); 3 foreach(var s in query) 4 { 5 Console.WriteLine(s); 6 } 7 /* 8 運行結果為: 9 10 ones 11 fre 12 fdfs 13 */
2.4 復合的from 子句
如果需要根據對象的一個成員進行篩選,而該成員本身是一個系列,就可以使用復合的from 子句。Racer 類定義了一個屬性Cars,其中Cars 是一個字符串數組。要篩選駕駛法拉利的所有冠軍,可以使用如下所示的LINQ 查詢。第一個from子句訪問從Formula1.GetChampion()
方法返回的Race 對象,第二個from 子句訪問Racer的 Cars 屬性。以返回所有string 類型的賽車。接着在where 子句中使用這些賽車篩選駕駛法拉利的所有冠軍。
1 var ferrariDrivers=from r in Formula.GetChampions() 2 from c in r.Cars 3 where c=="Ferrari" 4 orderby r.LastName 5 select r.FirstName +" "+ r.LastName;
C# 編譯器把符合的from 子句和LINQ 查詢轉換為SelectMany
擴展方法。其中實例所用的重載版本如下
1 public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult>(this IEnumerable<TSource> source,Func<TSource,IEnumberable<TCollection>> collectionSelector,Func<TSource,TCollection,TResult> resultSelector);
第一個參數是隱式參數,它從Get.Champions()
方法中接收Racer 對象序列。第二個參數是collectionSelector
委托,其中定義了內部序列。在Lambda 表達式r=>r.Cars
中,應返回賽車集合。第三個委托參數是一個委托,現在為每個賽車調用給委托,接收Racer 和Car 對象。Lambda 表達式創建了以匿名類型,他有Racer 和 Car 類型。 這個SelectMany
方法的結果是攤平了賽車手和賽車的層次結構,為每輛賽車返回匿名類型的一個新對象集合。
1 var ferrariDrivers= Formula1.GetChampion(). 2 SelectMany(r=>r.Cars, 3 (r,c)=>new{Racer=r,Car=c}. 4 where(r=>r.Car=="Ferrari"). 5 OrderBy(r=>r.Racer.LastName). 6 Select(r=>r.Racer.FirstName+" "+r.Racer.LastName));
2.5 排序
要對序列排序,前面使用了 orderby
子句。下面復習一下前面使用的orderby descending
子句的例子。其中賽車手按照贏得比賽的次數進行降序排序,贏得比賽的次數用關鍵字選擇器指定。
1 var racers=from r in Formula1.GetChampions() 2 where r.Country=="Brazil" 3 orderby r.Wins descending 4 select r;
orderby 子句解析為OrderBy( )
方法,orderby descending
子句解析為OrderByDescending
方法
1 var racers= Formula1.GetChampions(). 2 Where(r=>r.Country=="Brazil"). 3 OrderByDescending(r=>r.Wins). 4 Select(r=>r);
使用LINQ 查詢時,只需把所有用於排序的不同關鍵字(用逗號隔開)添加到orderby
子句中。在下例中,所有的賽車手先按照國家排序,再按照姓氏排序,最后按照名字排序。添加到LINQ 查詢結果中的Take()
擴展方法用於提取前十個結果:
1 var racers=(from r in Formula1.GetChampions() 2 orderby r.Country,r.LastName,r.FirstName 3 select r).Take(10);
使用OrderBy
和ThenBy
擴展方法可以執行相同的操作
1 var racers=Formula1.GetChamptions(). 2 OrderBy(r=>r.Country). 3 ThenBy(r=>r.LastName). 4 ThenBy(r=>r.FirstName). 5 Take(10);
2.6 分組
要根據一個關鍵字值對查詢結果分組,可以使用group
子句。 現在一級方程式冠軍應該按照國家分組,並列出一個國家的冠軍數。子句group r by r.County into g
根據 Country
屬性組合所有的賽車手,並定義一個新的標識符g
, 它以后用於訪問分組的結果信息。group
子句的結果應該根據應用到分組結果上的擴展方法Count
來排序,如果冠軍數相同,就根據關鍵字排序,該關鍵字是國家,因為這是分組使用的關鍵字。where 子句根據至少有兩項的分組來篩選結果。select 子句創建一個帶Country
和Count
屬性的匿名類型。
1 var countries= from r in Formula1.GetChampions() 2 group r by r.Country into g 3 orderby g.Count() descending, g.Key 4 where g.Count() >=2 5 select new { 6 Country=g.Key, 7 Count=g.Count() 8 }; 9 foreach(var item in countries) 10 { 11 Console.WriteLine("{1,-10} {1}",item.Country,item.Count); 12 }
接下來把子句 group r by r.Country into g
解析為GroupBy(r=>r.Country)
,返回分組序列。分組序列首先用OrderByDescending
方法排序,再用ThneBy
方法排序。接着調用Where
和Select
方法
1 var countries= Formula1.GetChampions(). 2 GroupBy(r=>r.Country). 3 OrderByDescending(g=>g.Count()). 4 ThenBy(g=>g.Key). 5 Where(g=>g.Count()>=2). 6 Select(g=>new {Country=g.Key,Count=g.Count()});
2.7 對嵌套的對象分組
如果分組的對象應包含嵌套的序列,就可以改變select
子句創建的匿名類型。在下面的例子中,所返回的國家不僅應包含國家名和賽車手數量這兩個屬性,還應包含賽車手名序列。這個序列用一個賦予Racers
屬性的from/ in
內部子句指定,內部的from 子句使用分組標識符g
獲得該分組中的所有賽車手,用姓氏對它們排序,再根據姓名創建一個新字符串。
1 var countries=from r in Formula1.GetChampions() 2 group r by r.Country into g 3 orderby g.Count() descending, g.Key 4 where g.Count()>=2 5 select new 6 { 7 Country=g.Key, 8 Count=g.Count(), 9 Racers=from r1 in g 10 orderby r1.LastName 11 select r1.FirstName +" "+ r1.LastName 12 }; 13 foreach(var item in countries) 14 { 15 Console.WriteLine("{0,-10} {1}",item.Country,item.Count); 16 foreach(var name in item.Racers) 17 { 18 Console.WriteLine("{0};",name); 19 } 20 Console.WirteLine(); 21 }
2.8 內連接
使用join 子句可以根據特定的條件合並兩個數據源,但之前要獲得兩個要連接的列表。在一級方程式比賽中,有賽車手冠軍和車隊冠軍。賽車手從GetChampions
方法中返回,車隊從GetConstructionChampions
方法中返回。現在要獲得一個年份列表,列出每年的賽車手冠軍和車隊冠軍。
1 var racers= from r in Formula1.GetChampions() 2 from y in r.Years 3 select new 4 { 5 Year=y, 6 Name=r.FirstName+" "+r.LastName 7 }; 8 9 vat teams=from t in Formula1.GetConstructorChampions() 10 from y in t.Years 11 select new 12 { 13 Year=y, 14 Name=t.Name 15 }; 16 var racersAndTeams=(from r in racers 17 join t in teams on r.Year equals t.Year 18 select new 19 { 20 r.Year, 21 Champion=r.Name, 22 Constructor=t.Name 23 }).Take(10); 24 Console.WriteLine("Year World Champion\t Constructor Title"); 25 foreach(var item in racersAndTeams) 26 { 27 Console.WriteLine("{0}:{1,-20} {2}",item.Year,item.Champion,item.Constructor); 28 }
或者合並成一個LINQ 查詢
1 var racersAndTeams=(from r in 2 from r1 in Formula1.GetChampions() 3 from yr in r1.Years 4 select new 5 { 6 Year=yr, 7 Name=r1.FirstName+" "+r1.LastName 8 } 9 join t in 10 from t1 in Formula1.GetConstructorChampions() 11 from yt in t1.Years 12 select new 13 { 14 Year=yt, 15 Name=t1.Name 16 } 17 on r.Year equals t.Year 18 orderby t.Year 19 select new 20 { 21 Year=r.Year, 22 Racer=r.Name, 23 Team=t.Name 24 }).Take(10);
2.9 左外連接
上一個連接示例的輸出從1958 年開始,因為從這一年開始,才同時有了賽車手冠軍和車隊冠軍。賽車手冠軍出現的更早一些,是在1950年。使用內連接時,只有找到了匹配的記錄才返回結果。為了在結果中包含所有的年份,可以使用左外聯接。左外連接返回左邊序列中的全部元素,即使它們在右邊的序列中並沒有匹配的元素。
下面修改前面的LINQ 查詢,使用左外連接。左外連接使用 join 子句和DefaultIfEmpty
方法定義。如果查詢的左側(賽車手)沒有匹配的車隊冠軍,那么就使用DefaultIfEmpty
方法定義其右側的默認值。
1 var racersAndTeams= 2 (from r in racers 3 join t in teams on r.Year equals t.Year into rt 4 from t in rt.DefaultIfEmpty() 5 orderby r.Year 6 select new 7 { 8 Year=r.Year, 9 Champion=r.Name, 10 Constructor=t==null?"no constructor championship":t.Name 11 }).Take(10);
2.10 組連接
左外連接使用了組連接和into 子句。它有一部分語法與組連接相同,只不過組連接不使用DefaultIfEmpty
方法。
使用組連接時,可以連接兩個獨立的序列,對於其中一個序列中的某個元素,另一個序列中存在對應的一個項列表。
下面的示例使用了兩個獨立的序列。一個是前面例子中已經看過的冠軍列表,另一個是一個ChampionShip
類型的集合。下面的代碼段顯示了Championship
類。
1 public class Championship 2 { 3 public int Year{get;set;} 4 public string First{get;set;} 5 public string Second{get;set;} 6 public string Third{get;set;} 7 }
GetChampionships
返回了冠軍集合
1 private static List<Championship> championships; 2 public static IEnumerable<Championship> GetChampionships() 3 { 4 if(championships == null) 5 { 6 championships=new List<Championship>(); 7 championships.Add(new Championship 8 { 9 Year=1950, 10 First="Nino Farina", 11 Second="Juan Manuel Fangio", 12 Third="Luigi Fagioli" 13 }); 14 championships.Add(new Championship 15 { 16 Year=1951, 17 First="Juan Manuel Fangio", 18 Second="Alberto Ascari", 19 Third="Froliab Gonzalez" 20 }); 21 }
冠軍列表應與每個冠軍年份中獲得前三名的賽車手構成的列表組合起來,然后顯示每一年的結果。
RacerInfo
類定義了要顯示的信息,如下所示:
1 public class RacerInfo 2 { 3 public int Year{get;set;} 4 public int Position {get;set;} 5 public string FirstName{get;set;} 6 public string LastName{get;set;} 7 }
使用連接語句可以把兩個列表中的賽車手組合起來。
因為冠軍列表中的每一項都包含三個賽車手,所以首先需要把這個這個列表攤平。一種方法是使用SelectMany
方法,該方法使用的Lambda 表達式為冠軍列表中的每一項返回包含三項的一個列表。在這個Lambda 表達式的實現中,因為RacerInfo 包含FirstName
和LastName
屬性,而收到的集合只包含帶有First 、Second、Third 屬性的一個名稱,所以必須拆分字符串,這可以通過擴展方法 FirstName
和SecondName
完成。
1 var racers=Formula1.GetChampionships() 2 .SelectMany(cs=>new List<RacerInfo>() 3 { 4 new RacerInfo{ 5 Year=cs.Year, 6 Position=1, 7 FirstName=cs.First.FirstName(), 8 LastName=cs.Last.LastName() 9 }, 10 new RacerInfo{ 11 Year=cs.Year, 12 Position=2, 13 FirstName=cs.Fisrt.FirstName(), 14 LastName=cs.Last.LastName() 15 }, 16 new RacerInfo{ 17 Year=cs.Year, 18 Position=3, 19 FirstName=cs.First.FirstName(), 20 LastName=cs.Last.LastName() 21 } 22 });
擴展方法FirstName 和SecondName 使用空格字符拆分字符串:
1 public static class StringExtension 2 { 3 public static string FirstName(this string name) 4 { 5 int ix=name.LastIndexOf(' '); 6 return name.Substring(0,ix); 7 } 8 public static string LastName(this string name) 9 { 10 int ix=name.LastIndexOf(' '); 11 return name.Substring(ix+1); 12 } 13 }
現在就可以連接兩個序列。Formula1.GetChampions
返回一個Racers 列表,racers 變量返回包含年份、比賽結果和賽車手名字的一個RacerInfo
列表。僅使用姓氏比較兩個集合中的項是不夠的。有時候列表中可能同時包含了一個賽車手和他的父親,所以必須同時使用FirstName
和LastName
進行比較。這是通過為兩個列表創建一個新的匿名類型實現的。通過使用into 子句,第二個集合中的結果被添加到了變量yearResults
中。對於第一個集合中的每一個賽車手,都創建了一個yearResults. 它包含了在第二個集合中匹配名和姓的結果。最后,用LINQ 查詢創建了一個包含所需信息的新匿名類型。
1 var q=(from r in Formula1.GetChampions() 2 join r2 in racers on 3 new 4 { 5 FirstName=r.FirstName, 6 LastName=r.LastName 7 } 8 equals 9 new 10 { 11 FisrtName=r2.FirstName, 12 LastName=r2.LastName 13 } 14 into yearResults 15 select new 16 { 17 FirstName=r.FirstName, 18 LastName=r.LastName, 19 Wins=r.Wins, 20 Stars=r.Stars, 21 Results=yearResults 22 }); 23 foreach(var r in q) 24 { 25 Console.WriteLine("{0} {1}",r.FirstName,r.LastName); 26 foreach(var results in r.Results) 27 { 28 Console.WriteLine("{0} {1}.",results.Year,results.Position); 29 } 30 }
2.11 集合操作
擴展方法 Distinct
、Union
、Intersect
、Except
都是集合操作。下面創建一個駕駛法拉利的一級方程式冠軍序列和駕駛邁凱倫的一級方程式冠軍序列,然后確定是否有駕駛法拉利和邁凱倫的冠軍。
1 var ferrariDrivers=from r in 2 Formula1.GetChampions() 3 from c in r.Cars 4 where c =="Ferrari" 5 orderby r.LastName 6 select r;
現在建立另一個基本相同的查詢,但where 子句的參數不同,以獲得所有駕駛邁凱倫的冠軍。最好不要再次編寫相同的查詢,而可以創建一個方法,其中給它傳遞參數 car
1 private static IEnumerable<Racer> GetRacersByCar(string car) 2 { 3 return from r in Formula1.GetChampions() 4 from c in r.Cars 5 where c==car 6 orderby r.LastName 7 select r; 8 }
但是,因為該方法不需要再其他地方使用,所以*應定義一個委托類型的變量來保存LINQ 查詢,racerByCar
變量必須是一個委托類型,該委托類型需要一個字符串參數,並返回IEnumerable<Racer>
,類似於前面實現的方法。為此,定義了幾個泛型委托Func<>
, 所以不需要聲明自己的委托。把一個Lambda 表達式賦予racerByCar
變量。Lambda 表達式的左邊定義了一個car 變量。其類型時Func 委托的第一個泛型參數(字符串)。右邊定義了LINQ 查詢,它使用該參數和where 子句:
1 Func<string , IEnumerable<Racer>> racersByCar= 2 car=>from r in Formula1.GetChampions() 3 from c in r.Cars 4 where c==car 5 orderby r.LastName 6 select r;
現在可以使用Intersect
擴展方法 ,獲得駕駛法拉利和邁凱倫的所有冠軍:
1 Console.WriteLine("World champion with Ferrari and McLaren"); 2 foreach(var racer in racersByCar("Ferraris").Interesect(racersByCar("McLaren"))) 3 { 4 Console.WirteLine(racer); 5 }
集合操作通過調用實體類的GetHashCode
和Equals
方法來比較對象。對於自定義比較,還可以傳遞一個實現了IEqualityComparer<T>
接口的對象。在這個示例中,GetChampions
方法總是返回相同的對象,因此默認的比較操作時有效的,如果不是這種情況,就可以重載集合方法來自定義比較操作。
2.12 合並
Zip()
方法,允許用一個謂詞函數把兩個相關的序列合並為一個。
首先,創建兩個相關的序列,它們使用相同的篩選和排序方法。對於合並,這很重要,因為第一個集合中的第一項會與第二個集合中的第一項合並,第一個集合中的第二項會與第二個集合中的第二項合並,以此類推。如果兩個序列的項數不同,Zip 方法就在到達較小集合的末尾時停止。
第一個集合中的元素有一個Name屬性,第二個集合中的元素有LastName 和Starts 兩個屬性
在racerNames集合上使用Zip 方法,需要把第二個集合(racerNamesAndStarts
)作為第一個參數。第二個參數的類型時Func<TFirst, TSecond, TResult>
這個參數實現為一個Lambda 表達式,它通過參數first 接收第一個集合的元素,通過參數second 接收第二個集合的元素。其實現代碼創建並返回一個字符串,該字符串包含第一個集合中元素的Name屬性和第二個集合中元素的Starts 屬性。
1 var racerNames=from r in Formula1.GetChampions() 2 where r.Country =="Italy" 3 orderby r.Wins descending 4 select new 5 { 6 Name=r.FirstName +" "+ r.LastName 7 }; 8 var racerNamesAndStarts=from r in Formula1.GetChampions() 9 where r.Country="Italy" 10 orderby r.Wins descending 11 select new 12 { 13 LastName=r.LastName, 14 Starts=r.Starts 15 }; 16 var racers=racerNames.Zip(racerNamesAndStarts,(first,second)=>first.Name+", starts: "+second.Starts); 17 foreach(var r in racers) 18 { 19 Console.WriteLine(r); 20 }
2.13 分區
擴展方法Take 和Skip 等的分區操作可用於分頁,例如在第一個頁面上只顯示5個賽車手,在下一個頁面上顯示接下來的5個賽車手。
在下面的LINQ 查詢中,把擴展方法Skip 和Take 添加到查詢的最后。Skip 方法先忽略根據頁面大小和實際頁數計算出的項數,再使用Take()
方法根據頁面大小提取一定數量的項。
1 int pageSize=5; 2 3 int numberPages=(int)Math.Ceiling(Formula1.GetChampions().Count()/(double)pageSize); 4 for(int page=0;page<numberPages;page++) 5 { 6 Console.WriteLine("Page {0}",page); 7 var racers=(from r in Formula1.GetChampions() 8 orderby r.LastName,r.FirstName 9 select r.FirstName+" "+r.LastName). 10 Skip(page*pageSize).Take(pageSize); 11 foreach(var name in racers) 12 { 13 Console.WriteLine(name); 14 } 15 Console.WriteLine(); 16 } 17
這個分頁機制的一個要點是,因為查詢會在每個頁面上執行,所以改變底層的數據會影響結果。在繼續執行分頁操作時,會顯示新對象。根據不同的情況,這對於應用程序可能有利。如果這個操作時不需要的,就可以只對原來的數據源分頁,然后使用映射導到原始數據上的緩存。
使用TakeWhile
和 SkipWhile
擴展方法,還可以傳遞一個謂詞,根據謂詞的結果提取或跳過某些項。
2.14 聚合操作符
聚合操作符(如 Count、Sum、 Min、Max、Average、Aggregate) 不返回一個序列,而返回一個值。
Count
擴展方法返回集合中的項數。下面的Count 方法應用於Racer 的Year 屬性,來篩選賽車手,只返回獲得冠軍次數超過三次的賽車手,因為同一個查詢中需要使用同一個計數超過一次,所以使用let 子句定義了一個變量 numberYear
1 var query=from r in Formula1.GetChampions() 2 let numberYears=r.Years.Count() 3 where numberYear>=3 4 orderby numberYears descending, r.LastName 5 select new 6 { 7 Name=r.FirstName+" "+r.LastName, 8 TimesChampion=numberYears 9 }; 10 foreach(var r in query) 11 { 12 Console.WriteLine("{0} {1}",r.Name,r.TimesChampion); 13 } 14
Sum 方法匯總序列中的所有數字,返回這些數字的和。下面的Sum 方法用於計算一個國家贏得比賽的總次數。首先根據國家對賽車手分組,再在新創建的匿名類型中,把Wins 屬性賦予某個國家贏得比賽的總次數。
1 var countries=(from c in from r in Formula1.GetChampions() 2 group r by r.Country into c 3 select new 4 { 5 Country=c.Key, 6 Wins=(from r1 in c select r1.Wins).Sum() 7 } 8 orderby c.Wins descending, c.Country 9 select c).Take(5); 10 foreach(var country in countries) 11 { 12 Console.WriteLine("{0} {1}",country.Country,country.Wins); 13 }
對於Aggergate
方法, 可以傳遞一個Lambda表達式,該表達式對所有的值進行聚合。
2.15 轉換操作符
前面提到,查詢可以推遲到訪問數據項時在執行。在迭代中使用查詢時,查詢會執行。而使用轉換操作符會立即執行查詢,把查詢結果放在數組、列表或字典中。
在下面的例子中,調用ToList 擴展方法,立即執行查詢,得到的結果放在List<T>
類中。
1 List<Racer> racers=(from r in Formula1.GetChampions() 2 where r.Starts>150 3 orderby r.Starts descending 4 select r).ToList(); 5 foreach(var racer in racers) 6 { 7 Console.WriteLine("{0} {0:S}",racer); 8 }
把返回的對象放在列表中並沒有那么簡單。例如,對於集合類中從賽車到賽車手的快速訪問。可以使用新類Lookup<TKey,TElement>
Dictionary<TKey,TValue>
類只支持一個鍵對應一個值。在System.Linq
名稱空間的類Lookup<TKey,TElement>
類中,一個鍵可以對應多個值。
使用復合的from 查詢,可以攤平賽車手和賽車序列,創建帶有Car 和Racer 屬性的匿名類型。在返回的Lookup 對象中,鍵的類型應是表示汽車的string
,值的類型應是Racer
。 為了進行這個選擇,可以給ToLookUp
方法的一個重載版本傳遞一個鍵和一個元素選擇器。鍵選擇器引用Car 屬性鎂元素選擇器引用Racer 屬性。
1 var racers=(from r in Formula1.GetChampions() 2 from c in r.Cars 3 select new 4 { 5 Car=c, 6 Racer=r 7 }).ToLookup(cr=>cr.Car,cr=>cr.Racer); 8 if(racers.Contains("Williams")) 9 { 10 foreach(var williamsRacer in Racers["Williams"]) 11 { 12 Console.WriteLine(williamsRacer); 13 } 14 }
如果需要在非類型化的集合上(如ArrayList
)使用LINQ 查詢,就可以使用Cast 方法。在下面的例子中,基於Object
類型的ArrayList
集合用Racer 對象填充。為了定義強類型化的查詢,可以使用Cast 方法
1 var list=new System.Collections.ArrayList(Formula1.GetChampions() as System.Collections.ICollection); 2 3 var query= from r in list.Cast<Racer>() 4 where r.Country=="USA" 5 orderby r.Wins descending 6 select r; 7 foreach(var racer in query) 8 { 9 Console.WriteLine("{0:A}",racer); 10 }
2.16 生成操作符
1
2 var values =Enumerable.Range(1,20); 3 foreach(var item in values) 4 { 5 Console.WriteLine("{0}",item); 6 } 7 Console.WriteLine(); 8 9 //結果 1 2 3 4 5 6 ...... 19 20
1 var values =Enumerable.Range(1,20).Select(n=>n*3);