• LINQ


    简介

    LINQ(Language Integrated Query,语言集成查询)在C#编程语言中集成了查询语法,可以用相同的语法访问不同的数据源。LINQ提供了不同数据源的抽象层,所以可以使用相同的语法。

    from r in Formula1.GetChampions()
        where r.Country == "Brazil"
        orderby r.Wins descending
        select r;
    

    子句from、where、orderby、descending和select都是这个查询中预定义的关键字。
    查询表达式必须以from子句开头,以select或group子句结束。在这两个子句之间,可以使用where、orderby、join、let和其他from子句。

    标准查询操作符

    标准查询操作符 说明
    Where
    OfType
    筛选操作符定义了返回元素的条件。在Where查询操作符中可以使用谓词,例如,lambda表达式定义的谓词,来返回布尔值。OfType根据类型筛选元素,只返回TResult类型的元素
    Select
    SelectMany
    投射操作符用于把对象转换为另一个类型的新对象。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都是限定符操作符。Any确定集合中是否有满足谓词函数的元素;All确定集合中的所有元素是否都满足谓词函数;Contains检查某个元素是否在集合中
    Take
    Skip
    TakeWhile
    SkipWhile
    分区操作符返回集合的一个子集。Take、Skip、TakeWhile和SkipWhile都是分区操作符。使用它们可以得到部分结果。使用Take必须指定要从集合中提取的元素个数;Skip跳过指定的元素个数,提取其他元素;TakeWhile提取条件为真的元素,SkipWhile跳过条件为真的元素
    Distinct
    Union
    Intersect
    Except
    Zip
    Set操作符返回一个集合。Distinct从集合中删除重复的元素。除了Distinct之外,其他Set操作符都需要两个集合。Union返回出现在其中一个集合中的唯一元素。Intersect返回两个集合中都有的元素。Except返回只出现在一个集合中的元素。Zip把两个集合合并为一个
    First
    FirstOrDefault
    Last
    LastOrDefault
    ElementAt
    ElementAtOrDefault
    Single
    SingleOrDefault
    这些元素操作符仅返回一个元素。First返回第一个满足条件的元素。FirstOrDefault类似于First,但如果没有找到满足条件的元素,就返回类型的默认值。Last返回最后一个满足条件的元素。ElementAt指定了要返回的元素的位置。Single只返回一个满足条件的元素。如果有多个元素都满足条件,就抛出一个异常。所有的XXOrDefault方法都类似于以相同前缀开头的方法,但如果没有找到该元素,它们就返回类型的默认值
    Count
    Sum
    Min
    Max
    Average
    Aggregate
    聚合操作符计算集合的一个值。利用这些聚合操作符,可以计算所有值的总和、所有元素的个数、值最大和最小的元素,以及平均值等
    ToArray
    AsEnumerable
    ToList
    ToDictionary
    Cast
    这些转换操作符将集合转换为数组:IEnumerable、IList、IDictionary等。Cast方法把集合的每个元素类型转换为泛型参数类型
    Empty
    Range
    Repeat
    这些生成操作符返回一个新集合。使用Empty时集合是空的;Range返回一系列数字;Repeat返回一个始终重复一个值的集合

    筛选

    使用where子句,可以合并多个表达式。例如,找出赢得至少15场比赛的巴西和奥地利赛车手。传递给where子句的表达式的结果类型应是布尔类型:

    var racers = from r in Formula1.GetChampions()
                where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")
                select r;
    foreach (var r in racers)
    {
      WriteLine($"{r:A}");
    }
    

    用索引筛选

    不能使用LINQ查询的一个例子是Where()方法的重载。在Where()方法的重载中,可以传递第二个参数——索引。索引是筛选器返回的每个结果的计数器。可以在表达式中使用这个索引,执行基于索引的计算。下面的代码由Where()扩展方法调用,它使用索引返回姓氏以A开头、索引为偶数的赛车手(代码文件EnumerableSample/Program.cs):

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

    注:索引及下标,入abc[2].

    类型筛选

    为了进行基于类型的筛选,可以使用OfType()扩展方法。这里数组数据包含string和int对象。使用OfType()扩展方法,把string类传送给泛型参数,就从集合中仅返回字符串:

    object[] data = { "one", 2, 3, "four", "five", 6 };
    var query = data.OfType<string>();
    foreach (var s in query)
    {
    	 WriteLine(s);
    }
    

    复合的from子句

    如果需要根据对象的一个成员进行筛选,而该成员本身是一个系列,就可以使用复合的from子句。Racer类定义了一个属性Cars,其中Cars是一个字符串数组。要筛选驾驶法拉利的所有冠军,可以使用如下所示的LINQ查询。第一个from子句访问从Formula1.Get Champions()方法返回的Racer对象,第二个from子句访问Racer类的Cars属性,以返回所有string类型的赛车。接着在where子句中使用这些赛车筛选驾驶法拉利的所有冠军(代码文件EnumerableSample/Program.cs)。

    var ferrariDrivers = from r in Formula1.GetChampions()
                        from c in r.Cars
                        where c == "Ferrari"
                        orderby r.LastName
                        select r.FirstName + " " + r.LastName;
    

    排序

    要对序列排序,前面使用了orderby子句。下面复习一下前面使用的例子,但这里使用orderby descending子句。其中赛车手按照赢得比赛的次数进行降序排序,赢得比赛的次数用关键字选择器指定:

    var racers = from r in Formula1.GetChampions()
                  where r.Country == "Brazil"
                  orderby r.Wins descending
                  select r;
    

    OrderBy()和OrderByDescending()方法返回IOrderEnumerable。这个接口派生自IEnumerable接口,但包含一个额外的方法CreateOrderedEnumerable()。这个方法用于进一步给序列排序。如果根据关键字选择器来排序,其中有两项相同,就可以使用ThenBy()和ThenByDescending ()方法继续排序。这两个方法需要IOrderEnumerable接口才能工作,但也返回这个接口。所以,可以添加任意多个ThenBy()和ThenByDescending()方法,对集合排序。

    使用LINQ查询时,只需要把所有用于排序的不同关键字(用逗号分隔开)添加到orderby子句中。在下例中,所有的赛车手先按照国家排序,再按照姓氏排序,最后按照名字排序。添加到LINQ查询结果中的Take()扩展方法用于返回前10个结果:

    var racers = (from r in Formula1.GetChampions()
                  orderby r.Country, r.LastName, r.FirstName
                  select r).Take(10);
    

    分组

    要根据一个关键字值对查询结果分组,可以使用group子句。现在一级方程式冠军应按照国家分组,并列出一个国家的冠军数。子句group r by r.Country into g根据Country属性组合所有的赛车手,并定义一个新的标识符g,它以后用于访问分组的结果信息。group子句的结果根据应用到分组结果上的扩展方法Count()来排序,如果冠军数相同,就根据关键字来排序,该关键字是国家,因为这是分组所使用的关键字。where子句根据至少有两项的分组来筛选结果,select子句创建一个带Country和Count属性的匿名类型。

    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()
                    };
    
    foreach (var item in countries)
    {
      WriteLine($"{item.Country, -10} {item.Count}");
    }
    

    例程:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    
    namespace CSharpTest
    {
        class progress
        {
            class Person
            {
                public string Name { set; get; }
                public int Age { set; get; }
                public string Gender { set; get; }
                public override string ToString() => Name;
            }
    
    
            public static int Main()
            {
                List<Person> personList = new List<Person>
                {
                    new Person
                    {
                        Name = "P1", Age = 18, Gender = "Male"
    
                    },
                    new Person
                    {
                        Name = "P2", Age = 19, Gender = "Male",
                    },
                    new Person
                    {
                        Name = "P2", Age = 17,Gender = "Female",
                    }
                };
    
                //var groups = personList.GroupBy(p => p.Gender);
                var groups = from p in personList
                             group p by p.Gender;
    
                foreach (var group in groups)
                {
                    Console.WriteLine(group.Key);
                    foreach (var person in group)
                    {
                        Console.WriteLine($"	{person.Name},{person.Age}");
                    }
                }
                return 0;
            }
        }
    }
    
    Male
      P1,18
      P2,19
    Female
      P2,17
    

    LINQ查询中的变量

    在为分组编写的LINQ查询中,Count方法调用了多次。使用let子句可以改变这种方式。let允许在LINQ查询中定义变量:

    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
                    };
    

    对嵌套的对象分组

    如果分组的对象应包含嵌套的序列,就可以改变select子句创建的匿名类型。在下面的例子中,所返回的国家不仅应包含国家名和赛车手数量这两个属性,还应包含赛车手的名序列。这个序列用一个赋予Racers属性的from/in内部子句指定,内部的from子句使用分组标识符g获得该分组中的所有赛车手,用姓氏对它们排序,再根据姓名创建一个新字符串:

    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
                    };
    
    foreach (var item in countries)
    {
      WriteLine($"{item.Country, -10} {item.Count}");
      foreach (var name in item.Racers)
      {
        Write($"{name}; ");
      }
      WriteLine();
    }
    

    内连接

    使用join子句可以根据特定的条件合并两个数据源,但之前要获得两个要连接的列表。在一级方程式比赛中,有赛车手冠军和车队冠军。赛车手从GetChampions()方法中返回,车队从GetConstructorChampions()方法中返回。现在要获得一个年份列表,列出每年的赛车手冠军和车队冠军。为此,先定义两个查询,用于查询赛车手和车队:

    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
                };
    

    有了这两个查询,再通过join子句,根据赛车手获得冠军的年份和车队获得冠军的年份进行连接。select子句定义了一个新的匿名类型,它包含Year、Racer和Team属性。

    var racersAndTeams = (from r in racers
                          join t in teams on r.Year equals t.Year
                          select new
                          {
                            r.Year,
                            Champion = r.Name,
                            Constructor = t.Name
                          }).Take(10);
    WriteLine("Year  World Champion	  Constructor Title");
    foreach (var item in racersAndTeams)
    {
      WriteLine($"{item.Year}: {item.Champion, -20} {item.Constructor}");
    }
    

    左外连接

    上一个连接示例的输出从1958年开始,因为从这一年开始,才同时有了赛车手冠军和车队冠军。赛车手冠军出现得更早一些,是在1950年。使用内连接时,只有找到了匹配的记录才返回结果。为了在结果中包含所有的年份,可以使用左外连接。左外连接返回左边序列中的全部元素,即使它们在右边的序列中并没有匹配的元素。
    下面修改前面的LINQ查询,使用左外连接。左外连接用join子句和DefaultIfEmpty方法定义。如果查询的左侧(赛车手)没有匹配的车队冠军,那么就使用DefaultIfEmpty方法定义其右侧的默认值:

    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);
    

    组连接

    左外连接使用了组连接和into子句。它有一部分语法与组连接相同,只不过组连接不使用DefaultIfEmpty方法。

    使用组连接时,可以连接两个独立的序列,对于其中一个序列中的某个元素,另一个序列中存在对应的一个项列表。

    TODO

    集合操作

    TODO

    合并

    Zip()方法允许用一个谓词函数把两个相关的序列合并为一个。

    首先,创建两个相关的序列,它们使用相同的筛选(国家意大利)和排序方法。对于合并,这很重要,因为第一个集合中的第一项会与第二个集合中的第一项合并,第一个集合中的第二项会与第二个集合中的第二项合并,依此类推。如果两个序列的项数不同,Zip()方法就在到达较小集合的末尾时停止。

    第一个集合中的元素有一个Name属性,第二个集合中的元素有LastName和Starts两个属性。

    在racerNames集合上使用Zip()方法,需要把第二个集合(racerNamesAndStarts)作为第一个参数。第二个参数的类型是Func<TFirst, TSecond, TResult>。这个参数实现为一个lambda表达式,它通过参数first接收第一个集合的元素,通过参数second接收第二个集合的元素。其实现代码创建并返回一个字符串,该字符串包含第一个集合中元素的Name属性和第二个集合中元素的Starts属性:

    var racerNames = from r in Formula1.GetChampions()
                    where r.Country == "Italy"
                    orderby r.Wins descending
                    select new
                    {
                    	Name = r.FirstName + " " + r.LastName
                    };
    
    var racerNamesAndStarts = from r in Formula1.GetChampions()
                              where r.Country == "Italy"
                              orderby r.Wins descending
                              select new
                              {
                                LastName = r.LastName,
                                Starts = r.Starts
                              };
    
    var racers = racerNames.Zip(racerNamesAndStarts,
        (first, second) => first.Name + ", starts: " + second.Starts);
    
    foreach (var r in racers)
    {
      WriteLine(r);
    }
    

    分区

    扩展方法Take()和Skip()等的分区操作可用于分页,例如,在第一个页面上只显示5个赛车手,在下一个页面上显示接下来的5个赛车手等。

    在下面的LINQ查询中,把扩展方法Skip()和Take()添加到查询的最后。Skip()方法先忽略根据页面大小和实际页数计算出的项数,再使用Take()方法根据页面大小提取一定数量的项:

    int pageSize = 5;
    int numberPages = (int)Math.Ceiling(Formula1.GetChampions().Count() / (double)pageSize);
    for (int page = 0; page < numberPages; page++)
    {
      WriteLine($"Page {page}");
      var racers = (from r in Formula1.GetChampions()
                    orderby r.LastName, r.FirstName
                    select r.FirstName + " " + r.LastName).
                    Skip(page * pageSize).Take(pageSize);
    
      foreach (var name in racers)
      {
        WriteLine(name);
      }
      WriteLine();
    }
    

    下面输出了前3页:

    Page 0
    Fernando Alonso
    Mario Andretti
    Alberto Ascari
    Jack Brabham
    Jenson Button
    
    Page 1
    Jim Clark
    Juan Manuel Fangio
    Nino Farina
    Emerson Fittipaldi
    Mika Hakkinen
    
    Page 2
    Lewis Hamilton
    Mike Hawthorn
    Damon Hill
    Graham Hill
    Phil Hill
    

    聚合操作符

    聚合操作符(如Count、Sum、Min、Max、Average和Aggregate操作符)不返回一个序列,而返回一个值。

    Count()扩展方法返回集合中的项数。下面的Count()方法应用于Racer的Years属性,来筛选赛车手,只返回获得冠军次数超过3次的赛车手。因为同一个查询中需要使用同一个计数超过一次,所以使用let子句定义了一个变量numberYears:

    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)
    {
      WriteLine($"{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)
    {
      WriteLine("{country.Country} {country.Wins}");
    }
    

    方法Min()、Max()、Average()和Aggregate()的使用方式与Count()和Sum()相同。Min()方法返回集合中的最小值,Max()方法返回集合中的最大值,Average()方法计算集合中的平均值。对于Aggregate()方法,可以传递一个lambda表达式,该表达式对所有的值进行聚合。

    转换操作符

    本章前面提到,查询可以推迟到访问数据项时再执行。在迭代中使用查询时,查询会执行。而使用转换操作符会立即执行查询,把查询结果放在数组、列表或字典中。

    在下面的例子中,调用ToList()扩展方法,立即执行查询,得到的结果放在List类中:

    List<Racer> racers = (from r in Formula1.GetChampions()
                            where r.Starts > 150
                            orderby r.Starts descending
                            select r).ToList();
    
    foreach (var racer in racers)
    {
      WriteLine($"{racer} {racer:S}");
    }
    

    生成操作符

    生成操作符Range()、Empty()和Repeat()不是扩展方法,而是返回序列的正常静态方法。在LINQ to Objects中,这些方法可用于Enumerable类。

    有时需要填充一个范围的数字,此时就应使用Range()方法。这个方法把第一个参数作为起始值,把第二个参数作为要填充的项数:

    var values = Enumerable.Range(1, 20);
      foreach (var item in values)
      {
        Write($"{item} ", item);
      }
      WriteLine();
    
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
    

    Empty()方法返回一个不返回值的迭代器,它可以用于需要一个集合的参数,其中可以给参数传递空集合。

    Repeat()方法返回一个迭代器,该迭代器把同一个值重复特定的次数。

    并行LINQ

    System.Linq名称空间中包含的类ParallelEnumerable可以分解查询的工作,使其分布在多个线程上。尽管Enumerable类给IEnumerable接口定义了扩展方法,但ParallelEnumerable类的大多数扩展方法是ParallelQuery类的扩展。一个重要的异常是AsParallel()方法,它扩展了IEnumerable接口,返回ParallelQuery类,所以正常的集合类可以以并行方式查询。

    并行查询

    为了说明并行LINQ(Parallel LINQ, PLINQ),需要一个大型集合。对于可以放在CPU的缓存中的小集合,并行LINQ看不出效果。在下面的代码中,用随机值填充一个大型的int集合:

    static IEnumerable<int> SampleData()
    {
      const int arraySize = 50000000;
      var r = new Random();
      return Enumerable.Range(0, arraySize).Select(x => r.Next(140)).ToList();
    }
    

    现在可以使用LINQ查询筛选数据,进行一些计算,获取所筛选数据的平均数。该查询用where子句定义了一个筛选器,仅汇总对应值小于20的项,接着调用聚合函数Sum()方法。与前面的LINQ查询的唯一区别是,这次调用了AsParallel()方法。

    var res = (from x in data.AsParallel()
                where Math.Log(x) < 4
                select x).Average();
    

    与前面的LINQ查询一样,编译器会修改语法,以调用AsParallel()、Where()、Select()和Average()方法。AsParallel()方法用ParallelEnumerable类定义,以扩展IEnumerable接口,所以可以对简单的数组调用它。AsParallel()方法返回ParallelQuery。因为返回的类型,所以编译器选择的Where()方法是ParallelEnumerable.Where(),而不是Enumerable.Where()。在下面的代码中,Select()和Average()方法也来自ParallelEnumerable类。与Enumerable类的实现代码相反,对于ParallelEnumerable类,查询是分区的,以便多个线程可以同时处理该查询。集合可以分为多个部分,其中每个部分由不同的线程处理,以筛选其余项。完成分区的工作后,就需要合并,获得所有部分的总和。

    var res = data.AsParallel().Where(x => Math.Log(x) < 4).
                                    Select(x => x).Average();
    

    运行这行代码会启动任务管理器,这样就可以看出系统的所有CPU都在忙碌。如果删除AsParallel()方法,就不可能使用多个CPU。当然,如果系统上没有多个CPU,就不会看到并行版本带来的改进。

    分区器

    AsParallel()方法不仅扩展了IEnumerable接口,还扩展了Partitioner类。通过它,可以影响要创建的分区。

    Partitioner类用System.Collection.Concurrent名称空间定义,并且有不同的变体。Create()方法接受实现了IList类的数组或对象。根据这一点,以及Boolean类型的参数loadBalance和该方法的一些重载版本,会返回一个不同的Partitioner类型。对于数组,使用派生自抽象基类OrderablePartitioner的DynamicPartitionerForArray类和StaticPartitionerFor-Array类。

    手工创建一个分区器,而不是使用默认的分区器:

    var result = (from x in Partitioner.Create(data, true).AsParallel()
                  where Math.Log(x) < 4
                  select x).Average();
    

    也可以调用WithExecutionMode()和WithDegreeOfParallelism()方法,来影响并行机制。对于WithExecutionMode()方法可以传递ParallelExecutionMode的一个Default值或者ForceParallelism值。默认情况下,并行LINQ避免使用系统开销很高的并行机制。对于WithDegreeOf Parallelism()方法,可以传递一个整数值,以指定应并行运行的最大任务数。查询不应使用全部CPU,这个方法会很有用。

    取消

    .NET提供了一种标准方式,来取消长时间运行的任务,这也适用于并行LINQ。

    要取消长时间运行的查询,可以给查询添加WithCancellation()方法,并传递一个CancellationToken令牌作为参数。CancellationToken令牌从CancellationTokenSource类中创建。该查询在单独的线程中运行,在该线程中,捕获一个OperationCanceledException类型的异常。如果取消了查询,就触发这个异常。在主线程中,调用CancellationTokenSource类的Cancel()方法可以取消任务。

    var cts = new CancellationTokenSource();
    Task.Run(() =>
    {
      try
      {
        var res = (from x in data.AsParallel().WithCancellation(cts.Token)
                  where Math.Log(x) < 4
                  select x).Average();
        WriteLine($"query finished, sum: {res}");
      }
      catch (OperationCanceledException ex)
      {
        WriteLine(ex.Message);
      }
    });
    
    WriteLine("query started");
    Write("cancel? ");
    string input = ReadLine();
    if (input.ToLower().Equals("y"))
    {
      // cancel!
      cts.Cancel();
    }
    
  • 相关阅读:
    KMP
    Trie 树
    Miller-Rabin质数测试
    快速幂
    Matlab 对图片的二值化处理
    huffman tree
    hdu5512-Pagodas
    迷宫
    poj2488-A Knight's Journey【DFS】
    linux操作
  • 原文地址:https://www.cnblogs.com/chendeqiang/p/12861605.html
Copyright © 2020-2023  润新知