书接上文,除了上一节中出现的Where等操作符外,
1、Enumerable类还有如下的标准查询操作符:
标准查询操作符 |
说明 |
Where OfType<TResult> |
筛选操作符,定义了返回元素的条件。Where可以使用谓词,例如lambda表达式定义的谓词,返回布尔值;OfType<TResult>根据类型筛选元素,只返回TResult类型的元素 |
Select SelectMany |
投射操作符,用于把对象转换为另外一个类型的新对象。他们定义了根据选择器函数选择结果值的投射 |
OrderBy ThenBy OrderByDescending ThenByDescending Reverse |
排序操作符,用于改变所返回元素的顺序。 OrderBy升序排列,OrderByDescending降序排列; 如果第一次排序的结果很类似,就可以使用ThenBy和ThenByDescending操作符进行第二次排序; Reverse反转集合中元素的顺序。 |
Join GroupJoin |
连接操作符,用于合并不直接相关的集合。Join操作符连接两个集合,类似于SQL中的JOIN;GroupJoin连接两个集合,组合其结果。 |
GroupBy ToLookup |
组合操作符,把数据放在组中。GroupBy组合有公共键的元素;ToLookup通过创建一个一对多的字典,来组合元素。 |
Any All Contains |
限定操作符,如果元素序列满足指定的条件,返回布尔值。Any确定集合中是否有满足谓词函数的元素;All确定集合中的所有元素是否否满足谓词函数;Contains检查某个元素是否在集合中。 |
Take Skip TakeWhile SkipWhile |
分区操作符,返回集合的一个子集。使用他们可以得到部分结果。使用Take必须制定要从集合中提取元素的个数;Skip跳过指定的元素个数,提取其他元素;TakeWHile提取条件为true的元素,SkipWhile跳过条件为true的元素。 |
Distinct Union Intersect Except Zip |
集合操作符,返回一个集合。Distinct从集合中删除重复的元素;除了Distinct,其他的操作都需要两个集合。Union返回出现在其中一个集合中的唯一元素;Intersect返回两个集合中都有的元素;Except返回只出现在一个集合中的元素;Zip把两个集合合并为一个。 |
First FirstOrDefault Last LastOrDefault ElementAt ElementAtOrDefault Single SingleOrDefault |
这些操作符都仅仅返回一个元素。First返回第一个满足条件的元素;FirstOrDefault类似于First,但如果没有找到满足条件的元素,就返回类型的默认值;Last返回最后一个满足条件的元素;ElementAt指定了要返回的元素的位置;Single只返回一个满足条件的元素,如果有对个元素都满足条件,就抛出异常。以OrDefault结尾的方法,都表示如果没有找到元素,则返回类型的默认值。 |
Count Min Max Sum Average Aggregate |
聚合操作符,计算集合的一个值。利用这些聚合操作符,可以计算所有值得总和、所有元素的个数、最大值、最小值、平均值。 |
ToArray AsEnumerable ToList ToDictionary Cast<TResult> |
转换操作符。可以将集合转换为数组、IEnumerable、IList、IDictionary等。Cast方法把集合的每个元素类型转换为泛型参数类型。 |
Empty Range Repeat |
生成操作符,返回一个新集合。使用Empty时集合是空的,Range返回一系列数字;Repeat返回一个始终重复一个值得集合 |
2、LINQ操作符的示例:
2.1 筛选
where可以合并多个表达式。例如找出至少赢得3场比赛的中国和美国选手:
1 private static void Filtering() 2 { 3 var racers = from r in Formulal.GetChampions() 4 where r.Wins > 3 && (r.Country == "中国" || r.Country == "美国") 5 select r; 6 foreach(var r in racers) 7 { 8 Console.WriteLine($"{r:A}"); 9 } 10 }
运行结果:
注意:并不是所有的查询都可以使用LINQ查询语法完成。也不是所有的扩展方法都映射到LINQ查询的子句上。高级查询需要使用扩展方法。最好看看简单的查询是如何映射的。使用Where()和Select(),会生成与前面LINQ查询非常类似的结果:
1 private static void FilteringWhithMethods() 2 { 3 var racers = Formulal.GetChampions() 4 .Where(r => (r.Country == "中国" || r.Country == "美国") && r.Wins > 3) 5 .Select(r => r); 6 foreach(var r in racers) 7 { 8 Console.WriteLine($"{r:A}"); 9 } 10 }
运行结果:
2.2 用索引筛选
不能使用LINQ查询的一个例子是Where()方法的重载。Where()有一个重载方法,可以传递第二个参数——索引。索引是筛选器返回的每个结果的计数器。可以在表达式中使用这个索引,执行基于索引的计算。下面的示例中代码由Where()扩展方法调用,它使用索引返回FirstName以“张”开头、索引为奇数的赛车手:
1 private static void FilteringWithIndex() 2 { 3 var racers = Formulal.GetChampions() 4 .Where((r, index) => r.FirstName == "张" && index % 2 == 0); 5 foreach(var r in racers) 6 { 7 Console.WriteLine($"{r:A}"); 8 } 9 }
运行结果:
特别强调:这里的索引是指在数据源(List)上的索引(源元素的索引),不是Where之后的索引。
2.3 类型筛选
OfType()类型筛选的扩展方法。下面的示例中,数组data包含string和int对象,使用OfType,把string类传送给泛型参数,就从集合中仅仅返回字符串:
1 private static void TypeFiltering() 2 { 3 object[] data = { "one", 2, 3, "four", "five", 6 }; 4 var query = data.OfType<string>(); 5 foreach(var d in data) 6 { 7 Console.WriteLine(d); 8 } 9 }
运行结果:
2.4 复合的from子句
如果需要根据对象的一个成员进行筛选, 而该成员本身是一个系列,就可以使用复合的from子句。Racer类定义了一个属性Cars,这是一个字符串数组。要筛选驾驶法拉利的所有冠军,可以下面的LINQ查询:
1 private static void CompoundFrom() 2 { 3 var falaliDrivers = from r in Formulal.GetChampions() 4 from c in r.Cars 5 where c == "法拉利" 6 orderby r.FirstName 7 select r.FirstName + " " + r.LastName+ " "+c; 8 foreach(var i in falaliDrivers) 9 { 10 Console.WriteLine($"{i:A}"); 11 } 12 }
运行结果:
编译器会把复合子句和LINQ查询转化为SelectMany()扩展方法。SelectMany()方法可用于迭代序列的序列。
2.5 SelectMany()扩展方法
作用:将序列的每个元素投影到IEnumerable<T>并将结果序列合并为一个序列。
理解重载的要点在于理解委托。
它有4个重载:
重载1:
SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>, Func<TSource, IEnumerable<TCollection>>,Func<TSource,TCollection,TResult>) ,函数声明为:
public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult> (this IEnumerable<TSource> source, Func<TSource,IEnumerable<TCollection>> collectionSelector, Func<TSource,TCollection,TResult> resultSelector);
作用:将序列的每个元素投影到IEnumerable<T>,并将结果序列合并为一个序列,并对其中每个元素调用结果选择器函数。
TSource:源序列的元素类型。TCollection:collectionSelector收集的中间元素的类型。TResult:结果序列的元素类型。collectionSelector:应用于输入序列的每个元素的转换函数。resultCollector:应用于中间序列的每个元素的转换函数。
返回IEnumerable<T>,其元素是通过以下方法得到的:对source的每个元素调用一对多转换函数collectionSelector,然后将这些元素的每一个元素及其对应的源元素映射到一个结果元素。
示例,对数组执行一对多投影,并使用结果选择器函数将源序列中的每个对应元素保留在对的最终调用范围内Select:
1 static void Main() 2 { 3 SelectManyEx3(); 4 } 5 public static void SelectManyEx3() 6 { 7 PetOwner[] petOwnerArray = 8 { 9 new PetOwner { Name = "haha", Pets = new List<string> { "a","A"} }, 10 new PetOwner { Name = "hehe", Pets=new List<string> { "bill","align"} }, 11 new PetOwner{Name="heihei",Pets=new List<string>{"add","ajax"}} 12 }; 13 //约定:PetOwner[]称为外部集合,Pets称为内部子集合 14 //理解原理:将子集合中的每个元素,通过一对多的函数,与对应的外部集合的元素映射成一对一的结果,并返回。 15 //深刻理解委托的用法 16 /** 17 形参a用于指定内部子集合 18 形参b,c分别代表外部集合的元素(PetOwner)和内部集合的元素(string)。最终以(b,c)的组合形式返回,返回的形参叫a 19 形参d,指向形参a 20 形参e,指向d,其实类型就是a,只是中间多了个where筛选的过程 21 */ 22 var query = petOwnerArray 23 .SelectMany(a => a.Pets, (b, c) => new { b, c }) 24 .Where(d => d.c.StartsWith("a")) 25 .Select(e => new { Owner = e.b.Name, Pet = e.c }); 26 foreach(var p in query) 27 { 28 Console.WriteLine(p); 29 } 30 } 31 class PetOwner 32 { 33 public string Name { get; set; } 34 public List<string> Pets { get; set; } 35 }
运行结果:
重载2:
SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>,Func<TSource,Int32,IEnymerable<TCollection>>,Func<TSource,TCollection,TResult>)
作用:将序列的每个元素投影到IEnumerable<T>,并将结果序列合并为一个序列,并对其中每个元素调用结果选择器函数。每个源元素的索引用于该元素的中间投影表。
重载3:
SelectMany<TSource,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TResult>>) 。方法声明为:
public static IEnumerable<TResult> SelectMany<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,IEnumerable<TResult>> selector);
作用:将序列的每个元素投影到IEnumerable<T>并将结果序列合并为一个序列。
示例,演示对数组执行一对多投影:
1 private static void SelectManyEx1() 2 { 3 PetOwner[] petOwnerArray = 4 { 5 new PetOwner { Name = "haha", Pets = new List<string> { "a","A"} }, 6 new PetOwner { Name = "hehe", Pets=new List<string> { "bill","align"} }, 7 new PetOwner{Name="heihei",Pets=new List<string>{"add","ajax"}} 8 }; 9 var query = petOwnerArray.SelectMany(a => a.Pets); 10 Console.WriteLine("SelectMany():"); 11 foreach (var p in query) 12 Console.WriteLine(p); 13 14 var query2 = petOwnerArray.Select(a => a.Pets); 15 Console.WriteLine("Select():"); 16 foreach (var p in query2) 17 foreach (var pIn in p) 18 Console.WriteLine(pIn); 19 //Console.WriteLine(); 20 }
运行结果:
重载4:
SelectMany<TSource,TResult>(IEnumerable<TSource>,Func<TSource,Int32,IEnumerable<TResult>>),函数声明为:
public static IEnumerable<TResult> SelectMany<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,int,IEnumerable<TResult>> selector);
selector Func<TSource,int,IEnumerable<TResult>> 一个应用于每个源元素的转换函数,这个函数有两个参数,第二个参数表示源元素的索引(从0开始)。
作用:将序列的每个元素投影到IEnumerable<T>并将结果序列合并为一个序列。每个源元素的索引用于该元素的投影表。
如果元素处于已知顺序,并且你想要对特定索引处的元素执行某些操作,这就会很有用;如果要检索一个或多个元素的索引,这也会很有用。
示例,对数组执行一对多的投影,并使用每个外部元素的索引:
1 private static void SelectManyEx2() 2 { 3 PetOwner[] petOwnerArray = 4 { 5 new PetOwner { Name = "haha", Pets = new List<string> { "a","A"} }, 6 new PetOwner { Name = "hehe", Pets=new List<string> { "bill","align"} }, 7 new PetOwner{Name="heihei",Pets=new List<string>{"add","ajax"}} 8 }; 9 var query = petOwnerArray.SelectMany((a, b) => a.Pets.Select(c => b + c)); 10 foreach (var p in query) 11 Console.WriteLine(p); 12 }
运行结果:
2.6 排序 orderby OrderBy
orderby:升序,orderby descending:降序。示例:
1 private static void SortDescending() 2 { 3 var descSort = from r in Formulal.GetChampions() 4 where r.Country == "中国" 5 orderby r.Wins descending 6 select r; 7 foreach (var i in descSort) 8 { 9 Console.WriteLine($"{i:A}"); 10 } 11 }
运行结果:
orderby和orderbydescending子句会被解析为OrderBy()和OrderByDescending()方法:
1 private static void SortDescendingWithMethod() 2 { 3 var descSort = Formulal.GetChampions() 4 .Where(r => r.Country == "中国") 5 .OrderByDescending(r => r.Wins) 6 .Select(r => r); 7 foreach (var i in descSort) 8 { 9 Console.WriteLine($"{i:A}"); 10 } 11 }
运行结果和orderby descending一致:
如果按照关键字选择器(这里是Country)来排序,其中两项相同,就可以使用ThenBy继续排序。可以使用任意多的ThenBy()方法对集合进行排序。orderby 子句中如果有多个关键字,相当于OrderBy(关键字1) ……ThenBy(关键字2) ThenBy(关键字3)
1 private static void SortMultiple() 2 { 3 var query = (from r in Formulal.GetChampions() 4 orderby r.Country, r.FirstName, r.Wins 5 select r.Country+" "+r.FirstName+""+r.Wins).Take(12); 6 foreach (var p in query) 7 Console.WriteLine($"{p:A}"); 8 Console.WriteLine("------------------------------"); 9 var query2 = Formulal.GetChampions() 10 .OrderBy(r => r.Country) 11 .ThenBy(r => r.FirstName) 12 .ThenBy(r => r.Wins) 13 .Select(r=>r.Country + " " + r.FirstName + "" + r.Wins) 14 .Take(12); 15 foreach (var p in query2) 16 Console.WriteLine($"{p:A}"); 17 }
运行结果:
2.7 分组 GroupBy
使用group子句,可以根据一个关键字对查询结果进行分组。示例,将冠军按照国家分组,并列出一个每个国家的冠军数量。
1 private static void Grouping() 2 { 3 var query = from r in Formulal.GetChampions() 4 group r by r.Country into g 5 orderby g.Count() descending, g.Key 6 where g.Count() >= 2 7 select new 8 { 9 CounytryNew = g.Key, 10 CountNum = g.Count() 11 }; 12 foreach (var p in query) 13 Console.WriteLine(p); 14 }
group r by r.Country into g :根据Country属性组合所有车手,并定义一个新的范围变量g,用它可以访问分组的结果信息;
orderby g.Count() descending, g.Key :根据分组结果g的扩展方法Count()进行降序排序,如果冠军数量相同,就根据关键字关键字排序(也就Country);
where :根据分组中的元素个数来筛选结果;
select :创建一个带CountryNew、CountNum属性的匿名类型。
运行结果:
想要使用扩展方法执行相同的结果,应该把groupby子句解析为GroupBy方法。注意,GroupBy方法会返回一个实现了IGrouping接口的枚举对象。IGrouping接口定义了Key属性,就可以在随后访问分组的关键字。代码:
1 private static void GroupingWithMethod() 2 { 3 var query = Formulal.GetChampions() 4 .GroupBy(r => r.Country) 5 .OrderByDescending(g => g.Count()) 6 .ThenBy(h => h.Key) 7 .Where(j => j.Count() >= 2) 8 .Select(k => new 9 { 10 CountryNew = k.Key, 11 CountNum = k.Count() 12 }); 13 foreach (var p in query) 14 Console.WriteLine(p); 15 }
2.8 LINQ查询中的变量 let
上述2.6示例中的分组代码,Count方法调用了多次。使用let子句可以改变使用方式。let允许在LINQ查询中定义变量:
1 private static void GroupingWithVariables() 2 { 3 var query = from r in Formulal.GetChampions() 4 group r by r.Country into g 5 let count = g.Count() 6 orderby count descending, g.Key 7 where count >= 2 8 select new 9 { 10 CountryNew = g.Key, 11 CountNum = count 12 }; 13 foreach (var p in query) 14 Console.WriteLine(p); 15 }
上述代码解析为扩展方法:
1 var query2 = Formulal.GetChampions() 2 .GroupBy(r => r.Country) 3 .Select(s => new { Group = s, Count = s.Count() }) 4 .OrderByDescending(t => t.Count) 5 .ThenBy(u => u.Group.Key) 6 .Where(v => v.Count >= 2) 7 .Select(w => new 8 { 9 Country = w.Group.Key, 10 Countww = w.Count 11 }); 12 foreach (var p in query2) 13 Console.WriteLine(p);
使用了Select方法创建一个匿名类型(包含Group和Count属性),这个类型定义了传递给下一个方法(OrderByDescending)的额外数据(let子句执行的操作)。
运行结果:
注意:应考虑根据let子句或Select方法创建的临时对象的数量。查询大列表时,创建的大量对象需要以后进行垃圾回收,这可能对性能产生巨大影响。
2.9 对嵌套对象的分组
如果分组的对象应该包含嵌套的序列,就可以改变select子句创建的匿名类型。示例,所返回的国家不仅应该包含国家和车手数量,还应该包含车手的姓名序列。这个序列用一个赋予Racers属性的from/in内部子句指定,内部的from子句使用分组标识符g获得该分组中的所有车手,用姓名进行排序,再根据姓名创建一个新字符串。如果使用扩展,内部对象使用IGrouping类型的group变量g创建的,其中Key属性是分组的键,可以使用Group属性访问组的项:
1 private static void GroupingAndNestedObjects() 2 { 3 //LINQ子句 4 var query1 = from r in Formulal.GetChampions() 5 group r by r.Country into g 6 let count = g.Count() 7 orderby count descending, g.Key 8 where count >= 2 9 select new 10 { 11 CountryNew = g.Key, 12 CountNum = count, 13 Racers = from s in g 14 orderby s.FirstName 15 select s.FirstName + "" + s.LastName 16 }; 17 foreach (var p in query1) 18 { 19 Console.WriteLine($"国家:{p.CountryNew,-10} 冠军数{p.CountNum}"); 20 Console.Write("冠军名单:"); 21 foreach(var i in p.Racers) 22 { 23 Console.Write($"{i} "); 24 } 25 Console.WriteLine(); 26 } 27 28 Console.WriteLine("-----------------------"); 29 var query2 = Formulal.GetChampions() 30 .GroupBy(r => r.Country) 31 .Select(s => new 32 { 33 Group = s, 34 Count = s.Count(), 35 Key = s.Key 36 }) 37 .OrderByDescending(t => t.Count) 38 .ThenBy(u => u.Key) 39 .Where(v => v.Count >= 2) 40 .Select(w => new 41 { 42 CountryNew = w.Key, 43 CountNum = w.Count, 44 Racers = w.Group.OrderBy(x => x.FirstName).Select(y => y.FirstName + y.LastName) 45 }); 46 foreach (var p in query2) 47 { 48 Console.WriteLine($"国家:{p.CountryNew,-10} 冠军数:{p.CountNum}"); 49 Console.Write("冠军名单:"); 50 foreach (var i in p.Racers) 51 { 52 Console.Write($"{i} "); 53 } 54 Console.WriteLine(); 55 } 56 }
运行结果:
2.10 内连接
使用join子句可以根据特定条件合并两个数据源。数据源中,既有车手冠军,也有车队冠军。车手从GetChampions()方法获取,车队从GetConstructorChampions()方法获得。现在要获得一个年份列表,列出每年的车手冠军和车队冠军。可以根据个人喜好,编写不同风格的LINQ语句。既可以先定义两个查询,分别查询车手和车队,然后通过join子句,根据车手获得冠军的年份和车队获得冠军的年份进行连接。也可以把它们合并为一个LINQ查询。同时,示例中也给出了扩展方法的形式:
1 private static void InnerJoin() 2 { 3 //1 LINQ查询 4 // 1.1 分成两个子查询的方式 5 Console.WriteLine("两个LINQ子查询的方式:"); 6 var racers = from r in Formulal.GetChampions() 7 from s in r.Years 8 select new 9 { 10 Year = s, 11 Name = r.FirstName //+ r.LastName 12 }; 13 var team = from r in Formulal.GetConstructorChampions() 14 from s in r.Years 15 select new 16 { 17 Year = s, 18 Name = r.Name 19 }; 20 var racersAndTeams = from r in racers 21 join s in team on r.Year equals s.Year 22 select new 23 { 24 ChamptionName = r.Name, 25 TeamName = s.Name, 26 Year = r.Year //s.Year 27 }; 28 foreach (var p in racersAndTeams) 29 Console.WriteLine($"年份:{p.Year} 车手冠军:{p.ChamptionName} 车队冠军:{p.TeamName}"); 30 // 1.2 合并为一个LINQ语句 31 Console.WriteLine("合并为一个LINQ语句:"); 32 var racerAndTeamsOver = 33 from c in 34 from r in Formulal.GetChampions() 35 from s in r.Years 36 select new 37 { 38 Yeay = s, 39 Name = r.FirstName 40 } 41 join t in from r in Formulal.GetConstructorChampions() 42 from s in r.Years 43 select new 44 { 45 Year = s, 46 Name = r.Name 47 } 48 on c.Yeay equals t.Year 49 orderby c.Yeay 50 select new 51 { 52 Year = c.Yeay, 53 ChampionName = c.Name, 54 TeamName = t.Name 55 }; 56 foreach (var p in racerAndTeamsOver) 57 Console.WriteLine($"年份:{p.Year} 车手冠军:{p.ChampionName} 车队冠军:{p.TeamName}"); 58 59 //2 扩展方法 60 Console.WriteLine("扩展方法:"); 61 var racersE = Formulal.GetChampions() 62 .SelectMany(r => r.Years, (s, year) => new 63 { 64 Year = year, 65 ChampionName = s.FirstName 66 }); 67 var teamsE = Formulal.GetConstructorChampions() 68 .SelectMany(r => r.Years, (s, year) => new 69 { 70 Year = year, 71 TeamName = s.Name 72 }); 73 var racersAndTeamsE = racersE 74 .Join(teamsE, r => r.Year, s => s.Year, (u, v) => new 75 { 76 Year = u.Year, 77 ChampionName = u.ChampionName, 78 TeamName = v.TeamName 79 }); 80 foreach (var p in racersAndTeamsE) 81 Console.WriteLine($"年份:{p.Year} 车手冠军:{p.ChampionName} 车队冠军:{p.TeamName}"); 82 }
运行结果:
内连接的结果与两个集合匹配,示意图:
2.10 左外连接 join……into
内连接时,只返回同时匹配两个集合的记录。为了在结果中显示所有的年份,可以使用左外连接。左外连接,返回左边集合中的全部元素,即使它在右边的集合中没有找到匹配的元素(相当于SQL中的LEFT JOIN)。示例,如果要查询左侧集合(车手)中没有匹配的右侧集合(车队)的冠军,就使用DefaultEmpty方法定义右侧的默认值。同时,我们也给出了扩展方法的实现:
1 //左外连接 2 private static void LeftOuterJoin() 3 { 4 //LINQ 5 var racers = from r in Formulal.GetChampions() 6 from s in r.Years 7 select new 8 { 9 Year = s, 10 RacerName = r.FirstName + r.LastName 11 }; 12 var teams = from r in Formulal.GetConstructorChampions() 13 from s in r.Years 14 select new 15 { 16 Year = s, 17 TeamName = r.Name 18 }; 19 var racerAndTeams = from r in racers 20 join t in teams on r.Year equals t.Year into rs 21 from s in rs.DefaultIfEmpty() 22 orderby r.Year 23 select new 24 { 25 Year = r.Year, 26 RacerName = r.RacerName, 27 Constructor = s == null ? "没有车队冠军" : s.TeamName 28 }; 29 foreach (var p in racerAndTeams) 30 Console.WriteLine($"年份:{p.Year} 车手冠军:{p.RacerName,-10} 车队冠军:{p.Constructor}"); 31 //扩展方法 32 //对应的使用GroupJoin方法。Join方法返回一个平铺列表,GroupJoin返回一个列表,其中第一个列表中包含的每个匹配项都包含第二个列表中的一个匹配列表。 33 //接着使用SelectMany方法,列表再次被平铺。 34 Console.WriteLine("扩展方法:"); 35 var racerandTeamE = racers 36 .GroupJoin( 37 teams, 38 r => r.Year, 39 s => s.Year, 40 (u, v) => new 41 { 42 Year = u.Year, 43 Racer = u.RacerName, 44 Consstructor = v 45 } 46 ) 47 .SelectMany( 48 w => w.Consstructor.DefaultIfEmpty(), 49 (x, y) => new 50 { 51 Year = x.Year, 52 RacerName = x.Racer, 53 Consstructor = y?.TeamName ?? "没有车队冠军" 54 }) 55 .OrderBy(z => z.Year); 56 foreach (var p in racerandTeamE) 57 Console.WriteLine($"年份:{p.Year} 车手冠军:{p.RacerName,-10} 车队冠军:{p.Consstructor}"); 58 }
运行结果:
2.11 组连接
组连接的语法和左外连接相似,只不过组连接不使用DefaultEmpty方法。组连接可以连接两个独立的序列,对于其中一个序列中的某个元素,另外一个序列中存在对应的一个项列表。示例中,除了之前已经使用的冠军列表,还新增了Championship类型的集合,它包含冠军年份以及该年份中获得第一名、第二名和第三名的车手;GetChampionships方法返回了冠军集合:
1 public class Championship 2 { 3 public int Year { get; } 4 public string First { get; } 5 public string Second { get; } 6 public string Third { get; } 7 8 public Championship(int year,string first,string second,string third) 9 { 10 Year = year; 11 First = first; 12 Second = second; 13 Third = third; 14 } 15 //冠军年份及其对应的前三名冠军 16 private static List<Championship> s_championships; 17 public static IEnumerable<Championship> GetChampionships() 18 { 19 if(s_championships == null) 20 { 21 s_championships = new List<Championship> 22 { 23 new Championship(1968,"张三","李四","王五"), 24 new Championship(1986,"李四","王五","赵六"), 25 new Championship(1952,"王五","赵六","张七") 26 }; 27 } 28 return s_championships; 2
冠军列表应该与每个冠军年份中的前三名车手连接起来,然后显示每一年的结果。
因为冠军年份中的每一项都包含3个车手,所以首先要把这个列表摊平。一种方式是使用复合的from子句。为了将三个属性(First、Second、Third)合并、摊平,这里新建了一个new List<T>用于填充这些属性信息。对于新创建的对象,可以使用自定义类和匿名类。这里通过创建元组的方式。元组包含不同类型的成员,可以使用带括号的元组字面量创建。其中,元组的一个摊平列表包含年份、冠军的位置、车手的姓名,并根据年份排序。存储为racers。
然后就可以开始连接两个序列了:
1 private static void GroupJoin() 2 { 3 var racers = from r in Championship.GetChampionships() 4 from s in new List<(int Year, int Position, string Name )>() 5 { 6 (r.Year,Position:1,Name:r.First), 7 (r.Year,Position:2,Name:r.Second), 8 (r.Year,Position:3,Name:r.Third) 9 } 10 orderby s.Year 11 select s; 12 var query = from r1 in Formulal.GetChampions() 13 join r2 in racers on r1.FirstName + r1.LastName equals r2.Name 14 into YearResult 15 select ( 16 r1.FirstName, 17 r1.LastName, 18 r1.Wins, 19 r1.Starts, 20 Results: YearResult 21 ); 22 foreach (var p in query) 23 { 24 Console.WriteLine($"{p.FirstName} {p.LastName} 获奖年份及名次:"); 25 foreach (var result in p.Results) 26 Console.WriteLine($"年份 {result.Year} 名次 {result.Position}"); 27 } 28 }
运行结果:
完成同样的查询操作,下面的示例使用了GroupJoin和扩展方法,SelectMany方法替代了复合from子句。调用GroupJoin方法时,传递车手作为第一个参数,把冠军和摊平的车手连接起来,用第二个和第三个参数来匹配两个集合。第四个参数接受第一个集合和第二个集合的赛车手。结果包含位置和年份,被写入Results元组成员:
1 private static void GroupJoinWithMethod() 2 { 3 var racers = Championship.GetChampionships() 4 .SelectMany(a => new List<(int Year, int Position, string Name)> { 5 (a.Year,Position:1,Name:a.First), 6 (a.Year,Position:2,Name:a.Second), 7 (a.Year,Position:3,Name:a.Third) 8 }) 9 .OrderBy(b=>b.Year); 10 var query = Formulal.GetChampions() 11 .GroupJoin(racers, 12 a => a.FirstName + a.LastName, 13 b => b.Name, 14 (c, d) => (c.FirstName, c.LastName, c.Wins, c.Starts, Results: d)); 15 foreach (var p in query) 16 { 17 Console.WriteLine($"{p.FirstName} {p.LastName} 获奖年份及名次:"); 18 foreach (var result in p.Results) 19 Console.WriteLine($"年份 {result.Year} 名次 {result.Position}"); 20 } 21 }
运行结果:
2.12 集合的交集操作
Distinct()、Union()、Intersect()、Except()都是集合操作。示例中,分别创建驾驶奥迪和法拉利的冠军序列,然后确定是否有同时驾驶奥迪和法拉利的冠军。当然,这里使用Intersect()方法。
可能我们的第一想法是为奥迪和法拉利驾驶者序列分别创建两个基本相同的查询(只是where子句中的变量的值不一样而已),但是,最好不要这样。像这种重复的功能,我们可以创建一个方法(如果其他地方不需要这个方法,我们就创建本地函数),给他传递一个参数(这里是car)。racerByCar是一个包含LINQ查询的lambda表达式的本地函数:
1 private static void SetOperations() 2 { 3 //获取驾驶奥迪和法拉利获取冠军的记录 4 //这是个本地函数,C#7的特性 5 IEnumerable<Racer> racersByCar(string car) => 6 from r in Formulal.GetChampions() 7 from c in r.Cars 8 where c == car 9 orderby r.LastName 10 select r; 11 12 Console.WriteLine("驾驶奥迪和法拉利获取冠军的人"); 13 foreach (var racer in racersByCar("法拉利").Intersect(racersByCar("奥迪"))) 14 { 15 Console.WriteLine(racer); 16 } 17 }
运行结果:
注意:Intersect()扩展方法是通过调用实体类的GetHashCode()和Equals()方法来比较对象的。对于自定义比较,还有传递一个实现了IEqualityComparer<T>接口的对象,参见官网Enumerable.Intersect 方法。而本示例中没有实现上述两个方法或者是传递接口对象,是因为Formulal.GetChampions()方法总是返回相同的对象,所以比较操作是有效的:
public static class Formulal { private static List<Racer> s_racers; public static IList<Racer> GetChampions() => s_racers ?? (s_racers = InitalizeRacers()); private static List<Racer> InitalizeRacers() => new List<Racer> { …… } …… }
2.13 合并 Zip()
Zip()方法允许使用一个谓词函数把两个相关的序列合并为一个。
首先,创建两个相关的序列,他们使用相同的筛选和排序方法。这点很重要,因为对于合并,第一个集合的第一项会与第二个集合的第一项合并,第一个集合的第二项会与第二个集合的第二项合并,以此类推。如果两个序列的项数不同,Zip()方法就在达到较小集合的末尾时停止。
1 private static void ZipOperation() 2 { 3 var racerName = from r in Formulal.GetChampions() 4 where r.Country == "中国" 5 orderby r.Wins descending 6 select new 7 { 8 Name = r.FirstName + r.LastName 9 }; 10 var racerNamesAndStarts = from r in Formulal.GetChampions() 11 where r.Country == "中国" 12 orderby r.Wins descending 13 select new 14 { 15 r.LastName, 16 r.Starts 17 }; 18 //Zip()有两个参数。第一个参数表示第二个集合;第二个参数类型是Func(TFirst,TSecond,TResult),这是个lambda表达式, 19 //它通过参数first接受第一个集合的元素,通过参数second接收第二个集合中的元素。 20 var racers = racerName.Zip(racerNamesAndStarts, (s, t) => s.Name + $",starts:{t.Starts}"); 21 //foreach (var p in racerName) 22 // Console.WriteLine(p.Name); 23 //foreach (var p in racerNamesAndStarts) 24 // Console.WriteLine(p.LastName + p.Starts); 25 foreach (var p in racers) 26 { 27 Console.WriteLine(p); 28 } 29 }
2.14 分区
Take()和Skip()方法的分区操作可用于分页。例如,在第一个页面上显示5个车手,在下一个页面显示接下来的5个车手。本示例中,Skip()和Take()方法添加到LINQ查询的最后,Skip()方法先忽略根据页面大小和实际页数计算出的项数,在使用Take()方法根据页面大小提取(相当于SQL中的Top关键字)
1 private static void PartitionOperation() 2 { 3 int pagesize = 5; 4 int numberPages = (int)Math.Ceiling(Formulal.GetChampions().Count()/(double)pagesize); 5 for (int page = 0;page < numberPages; page++) 6 { 7 Console.WriteLine($"------第{page}页-------"); 8 //Skip()方法先忽略根据页面大小和实际页数计算出的项数,再使用Take()方法根据页面大小提取一定数量的项 9 //如果注释掉Skip方法,则每页显示的内容都相同:都是总集合中的前pagesize个项 10 var racers = (from r in Formulal.GetChampions() 11 orderby r.FirstName, r.LastName 12 select r.FirstName + r.LastName) 13 .Skip(page * pagesize) 14 .Take(pagesize); 15 16 foreach (var p in racers) 17 Console.WriteLine($"车手:{p}"); 18 } 19 }
使用TakeWhile()和SkipWhile()方法,还可以传递一个谓词,根据谓词的提取或者跳过某些项
2.15 聚合操作符
聚合操作符,不返回一个序列,而是返回一个值。如
Count:返回集合中的项数
Sum:汇总序列中的所有数字,返回这些数字的和
Min:返回集合中的最小值
Max:返回集合中最大值
Average:返回集合中的平均值
Aggregate:可以传递一个lambda表达式,该表达式对所有的值进行聚合。
1 private static void AggregateOperation() 2 { 3 //Count ,返回获得冠军数超过3次的车手 4 var query = from r in Formulal.GetChampions() 5 let numYears = r.Years.Count() 6 where numYears >= 3 7 orderby numYears descending, r.FirstName 8 select new 9 { 10 Name = r.FirstName + r.LastName, 11 Times = numYears 12 }; 13 Console.WriteLine("---------Count 操作------------"); 14 foreach (var p in query) 15 Console.WriteLine(p); 16 17 //Sum ,计算一个国家赢得比赛的总次数 18 var query1 = from r in Formulal.GetChampions() 19 group r by r.Country into s 20 select new 21 { 22 Country = s.Key, 23 Wins = (from t in s 24 select t.Wins).Sum() 25 }; 26 Console.WriteLine("---------Sum 操作------------"); 27 foreach (var p in query1) 28 Console.WriteLine(p); 29 }
运行结果:
2.16 转换操作符
转换操作符会立即执行LINQ查询,把结果放在数组、列表、字典中。第一个示例中,使用了ToList()方法。第二个示例中,展示了更复杂点的查询,使用了Lookup类:复合的from子句,可以摊平车手和赛车序列,创建带有Car和Racer属性的匿名类型。在返回的Lookup对象中,键的类型应是表示汽车的string,值的类型应是Racer。为了进行这个选择,可以给ToLookup()方法的一个重载版本传递一个键和一个元素选择器。最后一个示例中,展示了Cast()方法的使用:在非类型化的集合上(如ArrayList)使用LINQ查询。
1 private static void ToList() 2 { 3 Console.WriteLine("-------ToList()----------"); 4 List<Racer> racers = (from r in Formulal.GetChampions() 5 where r.Starts > 20 6 orderby r.Starts descending 7 select r).ToList(); 8 foreach (var p in racers) 9 Console.WriteLine($"{p:S}"); 10 11 Console.WriteLine("-------ToLookup()----------"); 12 var racer = (from r in Formulal.GetChampions() 13 from s in r.Cars 14 select new 15 { 16 Car = s, 17 Racer = r 18 }).ToLookup(c => c.Car, c => c.Racer); 19 20 if (racer.Contains("法拉利")) 21 { 22 foreach (var p in racer["法拉利"]) 23 Console.WriteLine($"法拉利驾驶者:{p}"); 24 } 25 26 Console.WriteLine("-------Cast()----------"); 27 var list = new ArrayList(Formulal.GetChampions() as ICollection); 28 var query = from r in list.Cast<Racer>() 29 where r.Country == "中国" 30 orderby r.Wins descending 31 select r; 32 foreach (var p in query) 33 Console.WriteLine($"{p:A}"); 34 }
运行结果:
2.17 生成操作符
生成操作符Range()、Empty()、Repeat()不是扩展方法,而是返回序列的正常静态方法,可用于Enumerable类。
Range():填充一个范围的数字,可以使用Range()方法。这个方法把第一个参数作为起始值,第二个参数作为要填充的项数;
Empty():返回一个不返回值的迭代器,可以用于需要一个集合的参数,其中可以给参数传递空集合;
Repeat():返回一个迭代器,该迭代器把同一个值重复特定的次数。
示例:Range()
1 private static void GenerateRange() 2 { 3 var value = Enumerable.Range(1, 20); 4 foreach (var i in value) 5 Console.Write($"{i} "); 6 }
运行结果:
注意:Range()方法和其他方法一样,也会推迟执行查询,返回的是一个RangeEnumerator对象。其中只有一条yield return语句,用来递增值。
还可以把结果和其他扩展方法结合起来,获得另外一个结果:
1 private static void GenerateRange() 2 { 3 var value = Enumerable.Range(1, 20); 4 foreach (var i in value) 5 Console.Write($"{i} "); 6 Console.WriteLine(); 7 var value1 = Enumerable.Range(1, 20).Select(s=>s*3); 8 foreach (var i in value1) 9 Console.Write($"{i} "); 10 }
运行结果:
下一节,我们学习并行LINQ