c# 高级编程 12章249页 【LINQ】【标准查询操作符】

346 阅读5分钟
  • LINQ既定义了查询语法(是指:类似于SQL查询的LINQ查询子句where等)
  • LINQ也定义扩展方法(是指:查询操作符Where()等)
  • 并不是所有的查询都可以用LINQ查询语法完成
  • 并不是所有的LINQ扩展方法都可以映射到LINQ查询子句
  • 高级查询需要使用LINQ扩展方法

一些查询任务:

  1. 找出赢得至少15场比赛的巴西和奥地利赛车手
  2. 返回姓氏以A开头,索引为偶数的赛车手

Enumerable类定义的标准查询操作符

筛选操作

定义了返回元素的条件

  • 包括: Where()OfType<TResult>

Where

  • 可以使用谓词,返回bool
    • 例如:lamda表达式定义的谓词
  • Where(r => ...)
  • Where((r, index) => ...)筛选条件里可以使用索引index
        //使用LINQ扩展方法
        public static void FilteringWithMethods()
        {
            var racers = Formula1.GetChampions()
                .Where(r => r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria"));

            foreach (var r in racers)
            {
                Console.WriteLine($"{r:A}");
            }
        }

作为对比:

        //使用LINQ查询语法:where子句
        public static void Filtering()
        {
            var racers = from r in Formula1.GetChampions()
                         where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")
                         select r;

            foreach (var r in racers)
            {
                Console.WriteLine($"{r:A}");
            }
        }

用索引筛选:

        public static void FilteringWithIndex()
        {
            var racers = Formula1.GetChampions()
                .Where((r, index) => r.LastName.StartsWith("A") && index % 2 != 0);
            foreach (var r in racers)
            {
                Console.WriteLine($"{r:A}");
            }
        }

OfType<TResult>

  • 根据类型筛选元素
  • 值返回TRsult类型的元素
        public static void TypeFiltering()
        {
            object[] data = { "one", 2, 3, "four", "five", 6 };
            
            //筛选出string类型
            var query = data.OfType<string>();
            foreach (var s in query)
            {
                Console.WriteLine(s);
            }
        }

投射操作

对象转换成另一个类型的新对象

Select 和 SelectMany

  • 根据函数来投射
  • c#编译器会将复合from子句转化为SelectMany()扩展方法
  • SelectMany()可以用来迭代序列中的序列,例如:Racer中的Cars
  • SelectMany()的一个重载:
    • 入参1Func<TSource, IEnumerable<TCollection>> collectionSelector
      • 例如:r => r.Racers
      • 入参TSource类型, 即 Racer 类型的 r
      • 返回值IEnumerable<TCollection>类型, 即IEnumerable<string>类型的r.Racers
    • 入参2Func<TSource, TCollection, TResult> resultSelector
      • 例如: (r, c) => new { Racer = r, Car = c}
      • 入参是TSource类型和TCollection类型,即Racer rstring c
      • 返回值是TRsult类型,即匿名类型,包含两个属性RacerCar
        public static void CompoundFromWithMethods()
        {
            var ferrariDrivers = Formula1.GetChampions()
                .SelectMany(r => r.Cars, (r1, cars) => new { Racer1 = r1, Cars1 = cars })
                .Where(item => item.Cars1.Contains("Ferrari"))
                .OrderBy(item => item.Racer1.LastName)
                .Select(item => $"{item.Racer1.FirstName} {item.Racer1.LastName}");

        }

如果需要根据对象的一个成员进行筛选,而该成员本身是一个集合,就可以使用复合from子句

        public static void CompoundFrom()
        {
            //复合from子句
            var ferrariDrivers = from r in Formula1.GetChampions()
                                 from c in r.Cars
                                 where c == "Ferrari"
                                 orderby r.LastName
                                 select $"{r.FirstName} {r.LastName}";
        }

排序操作

排序查询操作符效果
OrderBy升序排序
OrderByDescending降序排序
ThenBy第一次排序,其中有两项相同的话,可以进行第二次排序,升序
ThenByDescending第一次排序,其中有两项相同的话,可以进行第二次排序,降序
Reverse翻转顺序
  • 上面这些扩展方法的返回值都是IOrderEnumerable<TSource>类型
    • 此类型派生自IEnumerable<TSource>
    • 多了一个CreateOrderedEnumerable<TSource>()方法
  • ThenByThenByDescending
    • 都需要在IOrderEnumerable<TSource>上工作
    • 同时也返回IOrderEnumerable<TSource>
    • 可以添加任意多个
  • orderby子句 解析为 OrderBy()方法
  • orderby descending子句 解析为 OrderByDescending()方法
        public static void SortDescendingWithMethods()
        {
            var racers = Formula1.GetChampions()
                .Where(r => r.Country == "Brazil")
                .OrderByDescending(r => r.Wins);
        }
        
        //可以添加任意多个ThenBy和ThenByDescending
        public static void SortMultipleWithMethods()
        {
            var racers = Formula1.GetChampions()
                            .OrderBy(r => r.Country)
                            .ThenBy(r => r.LastName)
                            .ThenBy(r => r.FirstName)
                            .Take(10);
        }
        public static void SortDescending()
        {
            var racers = from r in Formula1.GetChampions()
                         where r.Country == "Brazil"
                         orderby r.Wins descending
                         select r;
        }
        
        public static void SortMultiple()
        {
            var racers = (from r in Formula1.GetChampions()
                          orderby r.Country, r.LastName, r.FirstName
                          select r).Take(10);
        }

连接操作

合并不直接相关的集合

连接查询操作符效果
Join根据函数选择键,连接两个集合
GroupJoin根据函数选择键,连接两个集合,组合其结果

内连接

两个集合相当于求交集,得到c区域

image.png

扩展方法的写法:

        public static void InnerJoinWithMethods()
        {
            var racers = Formula1.GetChampions()
                .SelectMany(r => r.Years, (r1, year) =>
                new
                {
                    Year = year,
                    Name = $"{r1.FirstName} {r1.LastName}"
                });
			racers.Dump("将Racer按Year平铺");

            var teams = Formula1.GetConstructorChampions()
                .SelectMany(t => t.Years, (t, year) =>
                new
                {
                    Year = year,
                    t.Name
                });
			teams.Dump("将Team按Year平铺");

            var racersAndTeams = racers.Join(
                teams,
                r => r.Year,
                t => t.Year,
                (r, t) =>
                    new
                    {
                        r.Year,
                        Champion = r.Name,
                        Constructor = t.Name
                    }).OrderBy(item => item.Year);
			racersAndTeams.Dump("按Year合并");
        }

扩展方法的输出:

image.png

子句语法的写法:

         public static void InnerJoin()
        {
            var racers = from r in Formula1.GetChampions()
                         from y in r.Years
                         select new
                         {
                             Year = y,
                             Name = r.FirstName + " " + r.LastName
                         };
		    racers.Dump("将Racer按Year平铺");

            var teams = from t in Formula1.GetConstructorChampions()
                        from y in t.Years
                        select new
                        {
                            Year = y,
                            t.Name
                        };
			teams.Dump("将Team按Year平铺");

            var racersAndTeams =
                  (from r in racers
                   join t in teams on r.Year equals t.Year
                   orderby t.Year
                   select new
                   {
                       r.Year,
                       Champion = r.Name,
                       Constructor = t.Name
                   }).Take(10);
				   
	        racersAndTeams.Dump("按Year合并");
        }

左外连接

左外连接,包括了b,c 区域 image.png 扩展方法的写法中GroupJoin() 来实现左外连接

        public static void LeftOuterJoinWithMethods()
        {
            var racers = Formula1.GetChampions()
                .SelectMany(r => r.Years, (r1, year) =>
                new
                {
                    Year = year,
                    Name = $"{r1.FirstName} {r1.LastName}"
                });

            var teams = Formula1.GetConstructorChampions()
                .SelectMany(t => t.Years, (t, year) =>
                new
                {
                    Year = year,
                    Name = t.Name
                });

            var racersAndTeams =
                racers.GroupJoin(
                    teams,
                    r => r.Year,
                    t => t.Year,
                    (r, ts) => new
                    {
                        Year = r.Year,
                        Champion = r.Name,
                        Constructors = ts
                    }).Dump("GroupJoin执行结束,两个集合merge在了一起")
                    .SelectMany(
                        item => item.Constructors.DefaultIfEmpty(),  //取出merge完的表的item, 再取Constructors属性
                        (r, t) => new
                        {
                            Year = r.Year,
                            Champion = r.Champion,
                            Constructor = t?.Name ?? "no constructor championship"
                        });
			racersAndTeams.Dump("按Year, 左外连接Racer集合和Team集合");
        }

输出:

image.png

image.png

  • 子句语法的写法中,使用 join 子句DefaultIfEmpty 来实现左外连接
        public static void LeftOuterJoin()
        {
            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.GetConstructorChampions()
                        from y in t.Years
                        select new
                        {
                            Year = y,
                            t.Name
                        };

            var racersAndTeams =
              (from r in racers
               join t in teams on r.Year equals t.Year into rt   //用into, 给merge后的表一个名字rt
               from t in rt.DefaultIfEmpty()  //DefaultIfEmpty()表明进行左外连接
               orderby r.Year
               select new
               {
                   r.Year,
                   Champion = r.Name,
                   Constructor = t == null ? "no constructor championship" : t.Name   //指定,t没有对应的Year时,如何填充Year
               });
			racersAndTeams.Dump("按Year,将Racer和Team集合进行左外连接");
        }              

输出:

image.png

组连接

image.png

image.png

image.png

子句语法的写法:

        public static void GroupJoin()
        {
            var racers = from cs in Formula1.GetChampionships()
                         from r in new List<(int Year, int Position, string FirstName, string LastName)>()
                         {
                             (cs.Year, Position: 1, FirstName: cs.First.FirstName(), LastName: cs.First.LastName()),
                             (cs.Year, Position: 2, FirstName: cs.Second.FirstName(), LastName: cs.Second.LastName()),
                             (cs.Year, Position: 3, FirstName: cs.Third.FirstName(), LastName: cs.Third.LastName())
                         }
                         select r;
		    racers.Dump("将1,2,3名平铺");

            var q = (from r in Formula1.GetChampions()
                     join r2 in racers on    //使用了元组,同时比较FirstName和LastName
                     (
                         r.FirstName,
                         r.LastName
                     )
                     equals
                     (
                         r2.FirstName,
                         r2.LastName
                     )
                     into yearResults    //into子句,将第二个集合中的结果,添加到变量yearResults中
                     select    //又使用了元组,创建了包含所需信息的新元组类型
                     (
                         r.FirstName,
                         r.LastName,
                         r.Wins,
                         r.Starts,
                         Results: yearResults
                     ));
			q.Dump("各人的获奖情况");
        }

扩展方法的写法:

  • 第二个参数和第三个参数,用来匹配两个集合,来产生新的第二个集合?
  • 第四个参数接收第一个集合和第二个集合
        public static void GroupJoinWithMethods()
        {
            var racers = Formula1.GetChampionships()
              .SelectMany(cs => new List<(int Year, int Position, string FirstName, string LastName)>
              {
                 (cs.Year, Position: 1, FirstName: cs.First.FirstName(), LastName: cs.First.LastName()),
                 (cs.Year, Position: 2, FirstName: cs.Second.FirstName(), LastName: cs.Second.LastName()),
                 (cs.Year, Position: 3, FirstName: cs.Third.FirstName(), LastName: cs.Third.LastName())
              });

            var q = Formula1.GetChampions()
                .GroupJoin(racers,
                    r1 => (r1.FirstName, r1.LastName),
                    r2 => (r2.FirstName, r2.LastName),
                    (r1, r2s) => (r1.FirstName, r1.LastName, r1.Wins, r1.Starts, Results: r2s));

        }

组合操作

把数据放在组中

  • 根据一个关键字对查询结果分组 | 组合查询操作符 | 效果 | | --- | --- | | GroupBy | 组合有公共键的元素 | | ToLookup | 通过创建一个多对字典,来组合元素 |

GroupBy()

  • 入参:Func<TSource, TKey> keySelector
  • 返回值:IEnumerable<IGrouping<TKey, TSource>
    • 其中,IGrouping定义了属性Key,用来访问分组时指定的关键字
        public static void GroupingWithMethods()
        {
            var countries = Formula1.GetChampions()
              .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()
              });
        }

使用group子句

        public static void Grouping()
        {
            var countries = from r in Formula1.GetChampions()
                            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()
                            };
        }

操作分组后, 组中的items

  • 子句,可以在select子句上继续用操作组中的items
  • 扩展方法可以用IGroupingGroup属性来访问组中的items
        public static void GroupingAndNestedObjects()
        {
            var countries = from r in Formula1.GetChampions()
                            group r by r.Country into g
                            let count = g.Count()
                            orderby count descending, g.Key
                            where count >= 2
                            select new
                            {
                                Country = g.Key,
                                Count = count,
                                Racers = from r1 in g
                                         orderby r1.LastName
                                         select r1.FirstName + " " + r1.LastName
                            };
        }

        public static void GroupingAndNestedObjectsWithMethods()
        {
            var countries = Formula1.GetChampions()
                .GroupBy(r => r.Country)
                .Select(g => new
                {
                    Group = g,
                    g.Key,
                    Count = g.Count()
                })
                .OrderByDescending(g => g.Count)
                .ThenBy(g => g.Key)
                .Where(g => g.Count >= 2)
                .Select(g => new
                {
                    Country = g.Key,
                    g.Count,
                    Racers = g.Group.OrderBy(r => r.LastName).Select(r => r.FirstName + " " + r.LastName)
                });
        }

在LINQ查询中定义变量

  • 可以用let子句
  • 可以用Select()扩展方法来创建匿名类型
  • 不过,在查询大列表的时候,创建的大量变量,可能需要以后进行垃圾回收,对性能产生很大影响 如下例子:定义变量,避免多次调用Count()方法
        public static void GroupingWithVariables()
        {
            var countries = from r in Formula1.GetChampions()
                            group r by r.Country into g
                            let count = g.Count()    // let子句定义变量count
                            orderby count descending, g.Key
                            where count >= 2
                            select new
                            {
                                Country = g.Key,
                                Count = count
                            };
        }
        
        public static void GroupingWithAnonymousTypes()
        {
            var countries = Formula1.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 >= 2)
              .Select(g => new
              {
                  Country = g.Group.Key,
                  g.Count
              });
        }

限定操作

如果元素满足特定条件,限定操作符就返回布尔值

限定查询操作符效果
Any确定集合中是否有满足谓词的元素
All确定集合中的元素是否都满足谓词
Contains检查某个元素是否在集合中

分区操作

返回集合的一个子集

分区查询操作符效果
Take必须指定要从集合中提取的元素个数
Skip跳过指定个数个元素,提取其他元素
TakeWhile提取条件为真的元素。还可以传递一个谓词,根据谓词的结果提取
SkipWhile跳过条件为真的元素。还可以传递一个谓词,根据谓词的结果跳过

实现分页

  • TakeSkip结合起来,可以用来实现“分页
    • 分页在Web应用程序中很有用,可以只显示一部分数据给用户
    • 由于查询会【】在每个页面上执行,因此,改变底层的数据会影响结果
        public static void Partitioning()
        {
            int pageSize = 5;

            int numberPages = (int)Math.Ceiling(Formula1.GetChampions().Count() /
                  (double)pageSize);
	        numberPages.Dump("页数");

            for (int page = 0; page < numberPages; page++)
            {
                var racers =
                   (from r in Formula1.GetChampions()
                    orderby r.LastName, r.FirstName
                    select r.FirstName + " " + r.LastName).
                   Skip(page * pageSize).Take(pageSize);
                racers.Dump($"第{page+1}页");
            }
        }

Set操作符

返回一个集合

  • 集合操作通过实体类的GetHashCode()Equals()比较对象
  • 对于自定义的比较,还可以传递一个实现了IEqualityComparer<T>接口的对象
Set查询操作符效果
Distinct作用在单个集合上,从集合中删除重复元素
Union作用在两个集合上,返回出现在其中一个集合中的唯一元素
Intersect作用在两个集合上,返回两个集合中都出现的元素
Except作用在两个集合上,返回只出现在一个集合中的元素
Zip作用在两个集合上,把两个集合合并为一个

Intersect

//找出开法拉利和迈凯伦的冠军
        public static void SetOperations()
        {
            //定义了一个本地方法racersByCar,以方便在此小小作用域内重用
            IEnumerable<Racer> racersByCar(string car) =>
                from r in Formula1.GetChampions()
                from c in r.Cars
                where c == car
                orderby r.LastName
                select r;

            Console.WriteLine("World champion with Ferrari and McLaren");
            foreach (var racer in racersByCar("Ferrari").Intersect(racersByCar("McLaren")))
            {
                Console.WriteLine(racer);
            }
        }

Zip 合并

允许用一个谓词函数,把两个相关序列合并为一个

  • 这两个相关序列需要,使用相同的筛选和排序。这很重要,因为:
    • 两个集合的,第一项和第一项合并,第二项和第二项合并,以此类推
    • 如果项数不等,则到大项数较小的集合末尾时停止
  • Func<TFirst, TSecond, TResult>类型,用来定义如何合并TFirst为元素类型的集合和TSecond为元素类型的集合
    • 下例,实现为一个lamda表达式:(first, second) => first.Name + ", starts: " + second.Starts
        public static void ZipOperation()
        {
            var racerNames = from r in Formula1.GetChampions()
                             where r.Country == "Italy"
                             orderby r.Wins descending
                             select new
                             {
                                 Name = r.FirstName + " " + r.LastName
                             };
			racerNames.Dump("racerNames");

            var racerNamesAndStarts = from r in Formula1.GetChampions()
                                      where r.Country == "Italy"
                                      orderby r.Wins descending
                                      select new
                                      {
                                          r.LastName,
                                          r.Starts
                                      };
			racerNamesAndStarts.Dump("racerNamesAndStarts");

            var racers = racerNames.Zip(racerNamesAndStarts, (first, second) => first.Name + ", starts: " + second.Starts);
			
			racers.Dump("Zip之后的结果");
            
        }

输出:

image.png


元素操作符

仅返回一个元素

元素查询操作符效果
First返回第一个满足条件的元素
FirstOrDefault返回第一个满足条件的元素,如果没找到,就返回类型的默认值
Last返回最后一个满足条件的元素
LastOrDefault返回最后一个满足条件的元素, 如果没找到,就返回类型的默认值
ElementAt指定了要返回的元素的位置
ElementAtOrDefault指定了要返回的元素的位置, 如果没找到,就返回类型的默认值
Single只返回一个满足条件的元素,如果有多个元素满足条件,就抛出一个异常
SingleOrDefault只返回一个满足条件的元素,如果有多个元素满足条件,就抛出一个异常, 如果没找到,就返回类型默认值

聚合操作符

计算集合的一个值。它不返回一个序列,而是返回一个值。

  • Count
  • Sum
  • Min
  • Max
  • Average
  • Aggregate:可以传递一个lamda表达式,该表达式对所有值进行聚合
        public static void AggregateCount()
        {
            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
                        };
        }
        public static void AggregateSum()
        {
            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);
        }

转换操作符

将集合转换成数组:IEnumerable、IList、IDictionary等

  • ToArray
  • AsEnumerable
  • ToList
  • ToDictionary
  • Cast<TResult>

使用转换操作符会立即执行查询,将查询结果放到数组、列表或字典中

        public static void ToList()
        {
            //查询立即执行
            List<Racer> racers = (from r in Formula1.GetChampions()
                                  where r.Starts > 200
                                  orderby r.Starts descending
                                  select r).ToList();
        }

ToLookup()

  • 一个键,对应,多个值
  • cr => cr.Car 是键选择器
  • cr => cr.Racer 是元素选择器 ToLookup(cr => cr.Car, cr => cr.Racer)
        public static void ToLookup()
        {
            var racers = (from r in Formula1.GetChampions()
                          from c in r.Cars
                          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);
                }
            }
        }

Cast()

如果需要在非类型化集合(如 ArrayList)上使用LINQ查询,就可以使用Cast()

public static void ConvertWithCast()
        {
            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;
        }

生成操作符

返回一个新集合

  • Empty(), Range(), Repeat()
    • 不是扩展方法,而是正常的静态方法
    • 实际上,返回的不是填充了值的集合,而是返回迭代器。是推迟执行的查询,其中只有一个yield return语句,来递增值。
    • 可用于Enumerable
        public static void GenerateRange()
        {
            var values = Enumerable.Range(1, 20);
        }
var values = Enumerable.Range(1,20).Select(n => n * 3);

image.png

生成查询操作符效果
Empty空集合。返回一个不返回值的迭代器。常做入参用
Range返回一系列数字
Repeat返回一个始终重复一个值的集合