四、LINQ查询操作符实例
示例业务背景介绍
示例参考《C#高级编程(第六版)》LINQ章节(P267 - P296),进行改编。
打开示例代码我们看到:
- Racer.cs 文件,定义一级方程式世界车手冠军信息。
- Team.cs 文件,定义一级方程式世界车队冠军信息。
- Formula1.cs 文件,包含两个重要静态方法:(F1是"Formula One"的缩写)
- 1)GetChampions():返回一组车手列表。这个列表包含了1950到2007年之间的所有一级方程式世界车手冠军。
- 2)GetContructorChampions():返回一组车队列表。这个列表包含了1985到2007年之间的所有一级方程式世界车队冠军.
1、筛选操作符
where子句合并多个表达式。 根据条件返回匹配元素的集合IEnumerable<T>。
业务说明:找出赢得至少15场比赛的英国和奥地利赛车手。
var racers = from r in Formula1.GetChampions() where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria") select r; foreach (var racer in racers) { Console.WriteLine("{0:A}", racer); }
下面代码有Where扩展方法Where和Select调用。
var racers = Formula1.GetChampions(). Where(r => r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")). Select(r => r);
1、索引器筛选
索引是筛选器返回的每个结果的计数器。下面由Where扩展方法调用, 使用索引返回。
业务说明:找出姓氏以A开头、索引为偶数的赛车手
var query2 = Formual.GetChampions() .Where((r, index) => r.LastName.StartsWith("A") && index % 2 != 0);
2、类型筛选OfType<TResult>
基于类型筛选,使用 OfType 扩展方法。
业务说明:取出下面所有string类型的参数
object[] data = { "one", 1, 2, "li" }; var query = data.OfType<int>(); foreach (var intValue in query) { Console.WriteLine("{0}", intValue); }
2、投影操作符
1、Select 子句
将序列的每个元素经过lambda表达式处理后投影到一个新类型元素上。(与SelectMany不同在于,若单个元素投影到IEnumerable<TResult>,Select不会对多个IEnumerable<TResult>进行合并)
用Select方法创建匿名类型
var query11 = Formual.GetChampions() .GroupBy(r => r.Country) .Select(g => new { Group = g, Count = g.Count() }) .OrderByDescending(g => g.Count) .ThenBy(g => g.Group.Key) .Where(g => g.Count > 1) .Select(g => new
{ Country = g.Group.Key, Count = g.Count });
2、复合的From子句
业务说明:筛选驾驶法拉利的所有冠军
var ferrariDrivers = from r in Formula1.GetChampions() from c in r.Cars where c == "Ferrari" orderby r.LastName select r.FirstName + " " + r.LastName;
C#编译器会把复合的from语句转换为SelectMany方法。
var ferrariDrivers = Formula1.GetChampions()
.SelectMany(r => r.Cars, (r, c) => new { Racer = r, Car = c })
.Where(r => r.Car == "Ferrari")
.OrderBy(r => r.Racer.LastName)
.Select(r => r.Racer.FirstName + " " + r.Racer.LastName);
3、LINQ查询中的变量let子句
原来的语句:
var query8 = from r in Formual.GetChampions() group r by r.Country into g orderby g.Count() descending, g.Key where g.Count() > 1 select new { Country = g.Key, Count = g.Count() };
上面的分组查询Count方法调用了多次。使用let子句可以改变这种形式
var query10 = from r in Formual.GetChampions() group r by r.Country into g let count = g.Count() orderby count descending, g.Key where count > 1 select new { Country = g.Key, Count = count };
4、排序操作符
业务说明:来自英国的赛车手按照赢得比赛的次数进行降序排序
var query6 = from r in Formual.GetChampions() where r.Country == "UK" orderby r.Wins descending select r;
使用OrderBy(),OrderByDescending方式替换掉上面的写法:
var racers2 = Formula1.GetChampions() .Where(r => r.Country == "Brazil") .OrderByDescending(r => r.Wins) .Select(r => r);
1、使用ThenBy() 和 ThenByDescending() 方法继续排序进行二次排序
业务说明:获取车手冠军列表,并依次按照Country升序、LastName降序、FirstName升序进行排序。扩展方法 Take 提取前面 10 个元素。
// 查询表达式 var racers = (from r in Formula1.GetChampions() orderby r.Country, r.LastName descending, r.FirstName select r) .Take(10); // 方法语法 var racers = Formula1.GetChampions() .OrderBy(r => r.Country) .ThenByDescending(r => r.LastName) .ThenBy(r => r.FirstName).Take(10);; }
5、分组操作符
根据一个关键字值对查询结果进行分组,使用 group 子句。
子句 group r by r.Country into g 根据 Country 属性组合。并定义一个新的标识符g。它以后用于访问分组的结果信息。
业务说明:列出每个国家的冠军数
var countries = from r in Formual.GetChampions() group r by r.Country into g orderby g.Count() descending, g.Key where g.Count() > 1 select new { Country = g.Key, Count = g.Count() };
foreach (var country in countries) { Console.WriteLine(format: "{0,-10} {1}", arg0: country.Country, arg1: country.Count); }
使用扩展方法,子句 group r by r.Country into g 解析为 GroupBy(r => r.Country) 返回分组序列。
var countries = Formual.GetChampions() .GroupBy(r => r.Country) .OrderByDescending(g => g.Count()) .ThenBy(g => g.Key) .Where(g => g.Count() > 1) .Select(g => new { Country = g.Key, Count = g.Count() });
返回值为 IEnumerable<TResult>, (等价上面两种方式)
var countries = Formula1.GetChampions() .GroupBy(r => r.Country, (k, g) => new { Country = k, Racer = g }); .OrderByDescending(g => g.Count()) .ThenBy(g => g.Key) .Where(g => g.Count() > 1)
1、对嵌套的对象分组
分组的对象包含嵌套的序列,可以改变 select 子句创建的匿名类型。
业务说明:分组查询每个国家获得冠军的赛车手人数,人数必须大于2,返回国家名称、国家赛车手冠军数量、赛车手名称。
var query = from r in champions_racers group r by r.Country into g orderby g.Count() descending, g.Key where g.Count() >= 2 select new { Country = g.Key, count = g.Count(), RacerNickName = from r1 in g orderby r1.FirstName select r1.FirstName + " " + r1.LastName }; foreach (var g in query) { Console.WriteLine("Country={0},Count={1}", g.Country, g.count); foreach (var item in g.RacerNickName) { Console.WriteLine(item); } } //对应的扩展方法 var query = champions_racers.GroupBy(r => r.Country) .OrderByDescending(g => g.Count()) .ThenBy(g => g.Key) .Where(g => g.Count() >= 2) .Select(g => new { Country = g.Key, count = g.Count(), RacerNickName = g.OrderBy(r1 => r1.FirstName).Select(r1 => r1.FirstName + " " + r1.LastName) }); foreach (var g in query) { Console.WriteLine("Country={0},Count={1}", g.Country, g.count); foreach (var item in g.RacerNickName) { Console.WriteLine(item); } }
6、连表操作符
1、内连接
使用 join 子句 根据特定的条件合并两个数据源,但之前要获得两个要连接的列表。
业务说明:返回1958到1965年间的车手冠军和车队冠军信息,根据年份关联
var racers = from r in Formula1.GetChampions() from y in r.Years select new { Year = y, Name = r.FirstName + " " + r.LastName }; var teams = from t in Formula1.GetContructorChampions() from y in t.Years select new { Year = y, Name = t.Name }; var racersAndTeams0 = (from r in racers join t in teams on r.Year equals t.Year orderby t.Year select new { Year = r.Year, Racer = r.Name, Team = t.Name }).Take(10); // 方法语法 var racersAndTeams = racers.Join(teams , r => r.Year, t => t.Year , (r, t) => new { Year = r.Year, Racer = r.Name, Team = t.Name } );
或者合并成一个LINQ 查询
var racersAndTeams = (from r in from r1 in Formula1.GetChampions() from yr in r1.Years select new { Year = yr, Name = r1.FirstName + " " + r1.LastName } join t in from t1 in Formula1.GetContructorChampions() from yt in t1.Years select new { Year = yt, Name = t1.Name } on r.Year equals t.Year orderby t.Year select new { Year = r.Year, Racer = r.Name, Team = t.Name }).Take(10); // 方法语法 var racersAndTeams = Formual.GetChampions() .SelectMany(m => m.Years, (m, y) => new { Racer = m, Year = y }) .Join(Formual.GetContructorChampions() .SelectMany(m => m.Years, (m, y) => new { Team = m, Year = y }) , m => m.Year, m1 => m1.Year , (m, m1) => new { Year = m.Year, Racer = m.Racer.FirstName + " " + m.Racer.LastName, Team = m1.Team.Name }) .OrderBy(m => m.Year);
2、左外连接(DefaultIfEmpty)
左外连接返回左边序列中的全部元素,即使它们在右边的序列中并没有匹配的元素。
左外连接用join子句和 DefaultIfEmpty 方法定义。 使用 DefaultIfEmpty 定义其右侧的默认值。linq只支持左连接,如要右连接,将query和query1调换位置
业务说明:如赛车手比车队设立冠军的年份要早,可能某个年份只有赛车手冠军没有车队冠军,这时候需要左连接查询。
var racersAndTeams = (from r in racers join t in teams on r.Year equals t.Year into rt from t in rt.DefaultIfEmpty() orderby r.Year select new { Year = r.Year, Champion = r.Name, Constructor =t == null ? "no constructor championship" : t.Name }).Take(10);
3、组连接
左外连接使用了组连接和 into 子句。它有一部分与组连接相同,只不过组连接不适用 DefaultIfEmpty 方法。 使用组连接时,基于键相等对两个两个独立的序列的元素进行关联并对结果进行分组。常应用于返回“主键对象-外键对象集合”形式的查询。
业务说明:返回1958到1965年间的车手冠军和车队冠军信息,根据年份关联并分组
注意:直接出现在join子句之后的into关键字会被翻译为GroupJoin,而在select或group子句之后的into表示继续一个查询。
// 查询表达式 var racersAndTeams = from r in racers join t in teams on r.Year equals t.Year into groupTeams select new { Year = r.Year, Racer = r.Name, GroupTeams = groupTeams }; // 方法语法 var racersAndTeams = racers .GroupJoin(teams , r => r.Year, t => t.Year , (r, t) => new { Year = r.Year, Racer = r.Name, GroupTeams = t } );
join…on…equals…支持多个键关联,可以使用匿名类型来对多个键值进行Join,如下所示:
var query17 = from r in Formual.GetChampions() join r2 in query16 on new { FirstName = r.FirstName, LastName = r.LastName } equals new { FirstName = r2.FirstName, LastName =r2.LastName } into yearResults select new { FirstName = r.FirstName, LastName = r.LastName, Wins = r.Wins, Starts = r.Starts, Results = yearResults }; foreach (var item in query17) { Console.WriteLine($"FirstName:{item.FirstName};LastName:{item.LastName};Wins:{item.Wins};Starts:{item.Starts}"); foreach (var info in item.Results) { Console.WriteLine(info.LastName); } }
7、集合操作
扩展方法 Distinct()、 Union()、Intersect() 和 Except() 都是集合操作。
集合操作通过调用实体类的 GetHashCode() 和 Equals() 方法比较对象。 对于自定义比较,可以传递实现 IEqualityComparer<T>接口的对象。
- 1) Union:并集,返回两个序列的并集,去掉重复元素。
- 2) Concat:并集,返回两个序列的并集。
- 3) Intersect:交集,返回两个序列中都有的元素,即交集。
- 4) Except:差集,返回只出现在一个序列中的元素,即差集。
业务说明:获取使用车型”Ferrari”和车型”Mclaren”都获得过车手冠军车手列表
Func<string, IEnumerable<Racer>> racersByCar = car => from r in Formula1.GetChampions() from c in r.Cars where c == car orderby r.LastName select r; foreach (var racer in racersByCar("Ferrari").Intersect(racersByCar("McLaren"))) { Console.WriteLine(racer); }
- 5) Zip:通过使用指定的委托函数合并两个序列,集合的总个数不变。
示例:合并html开始标签和结束标签
string[] start = {"<html>","<head>","<body>" }; string[] end = { "</html>", "</head>", "</body>" }; var tags = start.Zip(end, (s, e) => { return s + e; }); foreach (string item in tags) { Console.WriteLine(item); }
- 6) SequenceEqual:判断两个序列是否相等,需要内容及顺序都相等。
示例:
int[] arr1 = { 1, 4, 7, 9 }; int[] arr2 = { 1, 7, 9, 4 }; Console.WriteLine("排序前 是否相等:{0}" , arr1.SequenceEqual(arr2) ? "是" : "否"); // 否 Console.WriteLine(); Console.WriteLine("排序后 是否相等:{0}" , arr1.SequenceEqual(arr2.OrderBy(k => k)) ? "是" : "否"); // 是
8、分区操作符
扩展方法 Take() 和 Skip() 等的分区操作可以用于分页。
添加在查询的“最后”,返回集合的一个子集。
- 1) Take:从序列的开头返回指定数量的连续元素。
- 2) TakeWhile:只要满足指定的条件,就会返回序列的元素。
- 3) Skip:跳过序列中指定数量的元素,然后返回剩余的元素。
- 4) SkipWhile:只要满足指定的条件,就跳过序列中的元素,然后返回剩余元素
业务说明:将车手冠军列表按每页5个名字进行分页。
int pageSize = 5; int numberPages = (int)Math.Ceiling( Formula1.GetChampions().Count() / (double)pageSize); for (int page = 0; page < numberPages; page++) { Console.WriteLine("Page {0}", page); var racers = ( from r in Formula1.GetChampions() orderby r.LastName select r.FirstName + " " + r.LastName ) .Skip(page * pageSize).Take(pageSize); foreach (var name in racers) { Console.WriteLine(name); } }
9、聚合操作符
聚合操作符(如 Count()、Sum()、Min()、Max()、Average() 、Aggregate() )返回一个值。
- Count 返回集合项数。LongCount:返回一个 System.Int64,表示序列中的元素的总数量。
业务说明:下面的Count 方法只返回获得冠军次数超过三次的赛车手,因为同一个查询中需要使用同一个计数超过一次,所以使用let 子句定义了一个变量 numberYear.
var query = from r in Formula1.GetChampions() let numberYears = r.Years.Count() where numberYears >= 3 orderby numberYears descending, r.LastName select new { Name = r.FirstName + " " + r.LastName, TimesChampion = numberYears }; foreach (var r in query) { Console.WriteLine("{0} {1}", r.Name, r.TimesChampion); }
- Sum 序列中的所有数字的和。
业务说明:下面的Sum 方法用于计算一个国家赢得比赛的总次数。首先根据国家对赛车手分组,再在新创建的匿名类型中,把Wins 属性赋予某个国家赢得比赛的总次数。
var countries = (from c in from r in Formula1.GetChampions() group r by r.Country into c select new { Country = c.Key, Wins = (from r1 in c select r1.Wins).Sum() } orderby c.Wins descending, c.Country select c).Take(5); foreach (var country in countries) { Console.WriteLine("{0} {1}", country.Country, country.Wins); }
- Min 返回集合中的最小值。
- Max 返回集合中的最大值。
- Average 返回集合中的平均值。
- Aggregate 传递一个 lambda 表达式,该表达式对所有的值进行聚合。
业务说明:Aggregate的第一个参数是算法的种子,即初始值。第二个参数是一个表达式,用来对每个元素进行计算(委托第一个参数是累加变量,第二个参数当前项)。第三个参数是一个表达式,用来对最终结果进行数据转换。
int[] numbers = { 1, 2, 3 }; // 1+2+3 = 6 int y = numbers.Aggregate((prod, n) => prod + n); // 0+1+2+3 = 6 int x = numbers.Aggregate(0, (prod, n) => prod + n); // (0+1+2+3)*2 = 12 int z = numbers.Aggregate(0, (prod, n) => prod + n, r => r * 2);
10、转换操作符
查询可以推迟到访问数据项时再执行。在迭代中使用查询时,查询会执行。而使用转换操作符会立即执行查询,把查询结果放在数组、列表或字典中。
LINQ本身支持四种不同的集合生成方式,包含生成数组的ToArray()
、生成列表的ToList
、生成字典集合的ToDictionary
以及生成Lookup<TKey,TElement>
类的ToLookup
。
- 1) Cast:将非泛型的 IEnumerable 集合元素转换为指定的泛型类型,若类型转换失败则抛出异常。
如果需要在非类型化的集合上(如ArrayList
)使用LINQ 查询,就可以使用Cast 方法。在下面的例子中,基于Object
类型的ArrayList
集合用Racer 对象填充。
var list = new System.Collections.ArrayList(Formula1.GetChampions() as System.Collections.ICollection); var query = from r in list.Cast<Racer>() where r.Country == "USA" orderby r.Wins descending select r; foreach (var racer in query) { Console.WriteLine("{0:A}", racer); }
- 2) ToArray:从 IEnumerable<T> 创建一个数组。
- 3) ToList:立即执行查询,从 IEnumerable<T> 创建一个 List<T>。
- 4) ToDictionary:根据指定的键选择器函数,从 IEnumerable<T> 创建一个 Dictionary<TKey,TValue>。
- 5) ToLookup:根据指定的键选择器函数,从 IEnumerable<T> 创建一个 System.Linq.Lookup<TKey,TElement>。
ToLookup使用比较复杂,Lookup类似于Dictionary,不过,Dictionary每个键只对应一个值,而Lookup则是1:n 的映射。Lookup没有公共构造函数,而且是不可变的。在创建Lookup之后,不能添加或删除其中的元素或键。(可以将ToLookup 视为GroupBy与ToDictionary的功能合体)
业务说明:将车手冠军按其使用车型进行分组,并显示使用”williams”车型的车手名字。
ILookup<string, Racer> racers = (from r in Formula1.GetChampions() from c in r.Cars //使用复合的from 查询 select new { Car = c, Racer = r } ).ToLookup(cr => cr.Car, cr => cr.Racer); if (racers.Contains("Williams")) { foreach (var williamsRacer in racers["Williams"]) { Console.WriteLine(williamsRacer); } }
- 6) DefaultIfEmpty:返回指定序列的元素;如果序列为空,则返回包含类型参数的默认值的单一元素集合。
var defaultArrCount = (new int[0]).DefaultIfEmpty().Count(); // 1
- 7) AsEnumerable:返回类型为 IEnumerable<T> 。用于处理LINQ to Entities操作远程数据源与本地集合的协作
11、生成操作符
生成操作符返回一个新的集合。三个生成操作符不是扩展方法,而是返回序列的正常静态方法。
- 1) Empty:生成一个具有指定类型参数的空序列 IEnumerable<T>。
Empty() 方法返回一个不返回值的迭代器,用于需要一个集合的参数,可以给参数传递空集合。
string[] names1 = { "Hartono, Tommy" }; string[] names2 = { "Adams, Terry", "Andersen, Henriette Thaulow", "Hedlund, Magnus", "Ito, Shu" }; string[] names3 = { "Solanki, Ajay", "Hoeing, Helge", "Andersen, Henriette Thaulow", "Potra, Cristina", "Iallo, Lucio" }; List<string[]> namesList = new List<string[]> { names1, names2, names3 }; IEnumerable<string> allNames = namesList.Aggregate(Enumerable.Empty<string>(), (current, next) => next.Length > 3 ? current.Union(next) : current); foreach (string name in allNames) { Console.WriteLine(name); }
- 2) Range:生成指定范围内的整数的序列 IEnumerable<Int32>。
如需要填充一二范围的数字,此时就应使用 Range() 方法。这个方法第一个参数作为起始值,把第二个参数作为要填充的项数。
var values = Enumerable.Range(1, 20); foreach (var value in values) { Console.WriteLine(value); }
//
结果 1 2 3 4 5 6 ...... 19 20
Range() 方法不返回填充所定义值的集合,与其他方法一样,推迟查询,返回一个 RangeEnumerator。其中用 yield return 语句,来递增值。该结果也可以与其他扩展方法一起用。
var values = Enumerable.Range(1, 20).Select(n => n * 3); foreach (var value in values) { Console.WriteLine(value); }
- 3) Repeat:生成包含一个重复值的序列 IEnumerable<T>。
Repeat() 方法 返回一个迭代器,把同一个值重复特定的次数。
IEnumerable<string> strings = Enumerable.Repeat("I like programming.", 15); foreach (String str in strings) { Console.WriteLine(str); }
12、量词操作符
如果元素序列满足指定的条件,量词操作符就返回布尔值。
1) Any:确定序列是否包含任何元素;或确定序列中的任何元素是否都满足条件。
2) All:确定序列中的所有元素是否满足条件。
3) Contains:确定序列是否包含指定的元素。
// 获取是否存在姓为“Schumacher”的车手冠军 var hasRacer_Schumacher = Formula1.GetChampions() .Any(r => r.LastName == "Schumacher");
13、元素操作符
这些元素操作符仅返回一个元素,不是IEnumerable<TSource>。(默认值:值类型默认为0,引用类型默认为null)
1) First:返回序列中的第一个元素;如果是空序列,此方法将引发异常。
2) FirstOrDefault:返回序列中的第一个元素;如果是空序列,则返回默认值default(TSource)。
3) Last:返回序列的最后一个元素;如果是空序列,此方法将引发异常。
4) LastOrDefault:返回序列中的最后一个元素;如果是空序列,则返回默认值default(TSource)。
5) Single:返回序列的唯一元素;如果是空序列或序列包含多个元素,此方法将引发异常。
6) SingleOrDefault:返回序列中的唯一元素;如果是空序列,则返回默认值default(TSource);如果该序列包含多个元素,此方法将引发异常。
7) ElementAt:返回序列中指定索引处的元素,索引从0开始;如果索引超出范围,此方法将引发异常。
8) ElementAtOrDefault:返回序列中指定索引处的元素,索引从0开始;如果索引超出范围,则返回默认值default(TSource)。
业务说明:获取冠军数排名第三的车手冠军
var Racer3 = Formula1.GetChampions() .OrderByDescending(r => r.Wins) .ElementAtOrDefault(2);
14、并行查询,并行Linq
AsParallel() 方法,扩展 IEnumerable<TSource> 接口,返回 ParallelQuery<TSource>类,所以正常的集合类可以以平行方式查询。
var query24 = from r in Formual.GetChampions().AsParallel() select r;
15、分区器
AsParallel()方法不仅扩展了 IEnumerable<T> 接口,还扩展了 Partitioner 类。通过它,可以影响创建的分区。
手动创建一个分区器
var query25 = from r in Partitioner.Create (Formual.GetChampions(), true).AsParallel() select r;
16、取消
.NET 提供一个标准方法,来取消长时间运行的任务,也适用于并行Linq。
要取消长时间运行的查询可以给查询添加WithCancellation() 方法,并传递一个 CancellactionToken令牌作为参数。CancelllationToken令牌从CancellactionTokenSource类中创建。该查询在单独的线程中运行,在该线程中,捕获一个OperationCanceledException类型的异常。如果取消了查询就出发这个异常。在主线程中,调用CancellationTokenSource类的Cancel()方法可以取消任务。
CancellationTokenSource cts = new CancellationTokenSource(); Task.Factory.StartNew(() => { try { var res = from r in Formual.GetChampions().AsParallel().WithCancellation(cts.Token) select r; Console.WriteLine("query finished, sum:{0}", res); } catch (OperationCanceledException ex) { Console.WriteLine("canceled!"); Console.WriteLine(ex.Message); } }); string input = Console.ReadLine(); if (input.ToLower().Equals("y")) { cts.Cancel(); Console.WriteLine("canceled 2!"); }