C#基础:LINQ 查询函数整理
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);