六、连表操作符

1、内连接

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 })     .OrderBy(p => p.Year).Take(10);


结果:


Year  Champion             Constructor Title
1958: Mike Hawthorn        Vanwall
1959: Jack Brabham         Cooper
1960: Jack Brabham         Cooper
1961: Phil Hill            Ferrari
1962: Graham Hill          BRM
1963: Jim Clark            Lotus
1964: John Surtees         Ferrari
1965: Jim Clark            Lotus
1966: Jack Brabham         Brabham
1967: Denny Hulme          Brabham


2、或者合并成一个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 racersAndTeams0 = Formula1.GetChampions()                .SelectMany(m => m.Years, (m, y) => new { Racer = m, Year = y })                .Join(Formula1.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).Take(10);


2、左外连接(DefaultIfEmpty)

左外连接返回左边序列中的全部元素,即使它们在右边的序列中并没有匹配的元素。

左外连接用join子句和 DefaultIfEmpty 方法定义。 使用 DefaultIfEmpty 定义其右侧的默认值。

linq只支持左连接,如要右连接,将query和query1调换位置

业务说明:如赛车手比车队设立冠军的年份要早,可能某个年份只有赛车手冠军没有车队冠军,这时候需要左连接查询。


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 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);  foreach (var item in racersAndTeams) {     Console.WriteLine("{0}: {1,-20} {2}", item.Year, item.Champion, item.Constructor); }


结果


1950: Nino Farina          no constructor championship
1951: Juan Manuel Fangio   no constructor championship
1952: Alberto Ascari       no constructor championship
1953: Alberto Ascari       no constructor championship
1954: Juan Manuel Fangio   no constructor championship
1955: Juan Manuel Fangio   no constructor championship
1956: Juan Manuel Fangio   no constructor championship
1957: Juan Manuel Fangio   no constructor championship
1958: Mike Hawthorn        Vanwall
1959: Jack Brabham         Cooper


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                      }).Take(10);


方法语法:


var racersAndTeams1 = racers     .GroupJoin(teams, r => r.Year, t => t.Year, (r, t) => new { Year = r.Year, Racer = r.Name, GroupTeams = t }     ).Take(10);;  foreach (var item in racersAndTeams) {     Console.WriteLine("{0}: {1,-20} {2}", item.Year, item.Racer, item.GroupTeams.Count()); }


结果:


1950: Nino Farina          0
1952: Alberto Ascari       0
1953: Alberto Ascari       0
1951: Juan Manuel Fangio   0
1954: Juan Manuel Fangio   0
1955: Juan Manuel Fangio   0
1956: Juan Manuel Fangio   0
1957: Juan Manuel Fangio   0
1958: Mike Hawthorn        1
1961: Phil Hill            1


2、join…on…equals…支持多个键关联,可以使用匿名类型来对多个键值进行Join,如下所示:


// 查询表达式 var query17 = from r in racers               join r2 in teams on new { Name = r.Name.Substring(0, 1), Year = r.Year } equals new { Name = r2.Name.Substring(0, 1), Year = r2.Year } into yearResults               select new               {                   Results = yearResults               };  foreach (var item in query17) {     foreach (var info in item.Results)     {         Console.WriteLine(info.Name);     } } //McLaren


七、集合操作

集合操作通过调用实体类的 GetHashCode() 和 Equals() 方法比较对象。 对于自定义比较,可以传递实现 IEqualityComparer接口的对象。


  • 1) Union:并集,返回两个序列的并集,去掉重复元素。
  • 2) Concat:连接,返回两个序列的并集。
  • 3) Intersect:交集,返回两个序列中都有的元素,即交集。
  • 4) Except:差集,返回只出现在一个序列中的元素,即差集。
  • 业务说明:获取使用车型”Ferrari”和车型”Mclaren”都获得过车手冠军车手列表


  • void Main() {     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"), new RacerComparer()))     {         Console.WriteLine(racer);     } }   public class RacerComparer : IEqualityComparer<Racer> {     public bool Equals(Racer x, Racer y)     {         if (Object.ReferenceEquals(x, y)) return true;         return x != null && y != null && x.FirstName == y.FirstName && x.LastName == y.LastName;     }      public int GetHashCode(Racer obj)     {         int hashStudentId = obj.FirstName.GetHashCode();         int hashScore = obj.LastName.GetHashCode();         return hashStudentId ^ hashScore;     } }


  • 结果:


  • Niki Lauda




  • 5) Zip:通过使用指定的委托函数合并两个序列,集合的总个数不变。

示例:合并html开始标签和结束标签


var letters = new string[] { "A", "B", "C", "D", "E" };
var numbers = new int[] { 1, 2, 3 };
var q = letters.Zip(numbers, (l, n) => l + n.ToString());
foreach (var s in q)
Console.WriteLine(s);


结果:


A1
B2
C3




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)) ? "是" : "否"); // 是



八、分区操作符

扩展方法 Take() 和 Skip() 等的分区操作可以用于分页。

添加在查询的“最后”,返回集合的一个子集。

1、Take()

从序列的开头返回指定数量的连续元素。

2、TakeWhile()

只要满足指定的条件,就会返回序列的元素。

从第一个元素开始, 读取Starts小于40的人员列表,只要遇到大于40的元素就立即停止返回。



  • var racers = (from r in Formula1.GetChampions()               orderby r.Starts               select r               )               .TakeWhile(p => p.Starts < 40);  foreach (var name in racers) {     Console.WriteLine($"{name:A}"); }



结果:


Alberto Ascari, Italy; starts: 32, wins: 10
Nino Farina, Italy; starts: 33, wins: 5


3、Skip()

跳过序列中指定数量的元素,然后返回剩余的元素。

业务说明:将车手冠军列表按每页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);
}
}


结果:


Page 0
Fernando Alonso
Mario Andretti
Alberto Ascari
Jack Brabham
Jim Clark
Page 1
Juan Manuel Fangio
Nino Farina
Emerson Fittipaldi
Mika Hakkinen
Mike Hawthorn



4、SkipWhile():

只要满足指定的条件,就跳过序列中的元素,然后返回剩余元素。

九、聚合操作符

聚合操作符返回一个值。

1、Count: 返回集合项数。

2、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); } //Michael Schumacher 7 //Juan Manuel Fangio 5 //Alain Prost 4 //Jack Brabham 3 //Niki Lauda 3 //Nelson Piquet 3 //Ayrton Senna 3 //Jackie Stewart 3


3、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); } //UK 138 //Germany 91 //Brazil 78 //France 51 //Finland 40


4、Min: 返回集合中的最小值。

5、Max: 返回集合中的最大值。

6、Average: 返回集合中的平均值。

7、Aggregate: 传递一个 lambda 表达式,该表达式对所有的值进行聚合。

业务说明:Aggregate的

第一个参数是算法的种子,即初始值。(可选)

第二个参数是一个表达式,用来对每个元素进行计算(委托第一个参数是累加变量,第二个参数当前项)。

第三个参数是一个表达式,用来对最终结果进行数据转换


int[] numbers = { 1, 2, 3 }; int y = numbers.Aggregate((tol, n) => prod + n); // 1+2+3 = 6 int x = numbers.Aggregate(0, (tol, n) => tol + n); // 0+1+2+3 = 6 int z = numbers.Aggregate(0, (tol, n) => tol + n, r => r * 2);// (0+1+2+3)*2 = 12


十、转换操作符

查询可以推迟到访问数据项时再执行。在迭代中使用查询时,查询会执行。

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

LINQ本身支持四种不同的集合生成方式,包含生成数组的​​ToArray()​​​、生成列表的​​ToList​​​、生成字典集合的​​ToDictionary​​​ 以及生成​​Lookup<tkey,telement>​​​类的​​ToLookup​​。

1) Cast:

将非泛型的 IEnumerable 集合元素转换为指定的泛型类型,若类型转换失败则抛出异常。

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

在下面的例子中,基于​​Object​​​类型的​​ArrayList​​集合用Racer对象填充。



var list = new ArrayList(Formula1.GetChampions() as System.Collections.ICollection);  var query = from r in list.Cast()             where r.Country == "USA"             orderby r.Wins descending             select r; foreach (var racer in query) {     Console.WriteLine("{0:A}", racer); } //Mario Andretti, USA; starts: 128, wins: 12 //Phil Hill, USA; starts: 48, wins: 3


2) ToArray:

从 IEnumerable 创建一个数组。

3) ToList:

立即执行查询,从 IEnumerable 创建一个 List。

4) ToDictionary:

根据指定的键选择器函数,从 IEnumerable 创建一个 Dictionary<tkey,tvalue>。

将列表转换为字典:


var spartans = new List<dynamic>         {             new {Opponent="UAB",Score="55-18"},             new {Opponent="Bowling Green",Score="55-18"},             new {Opponent="Pittsburgh",Score="55-18"},             new {Opponent="Notre Dame",Score="55-18"}         }; //字典是一种键值对的集合,ToDictionary 将一个IEnumerable<T>对象(比如LINQ查询所返回的结果) //转换为一个IDictionary<Key,Value>对象。 IDictionary<string, dynamic> stats = spartans.ToDictionary(key => (string)key.Opponent); Console.WriteLine("Spartans vs. {0} {1}", stats["Notre Dame"].Opponent, stats["Notre Dame"].Score);


5) ToLookup:

根据指定的键选择器函数,从 IEnumerable 创建一个 System.Linq.Lookup。

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);     } } //Alan Jones //Keke Rosberg //Nelson Piquet //Nigel Mansell //Alain Prost //Damon Hill //Jacques Villeneuve


6) DefaultIfEmpty:

返回指定序列的元素;如果序列为空,则返回包含类型参数的默认值的单一元素集合 。


var defaultArrCount = (new int[0]).DefaultIfEmpty().Count(); Console.WriteLine(defaultArrCount); //1


7) AsEnumerable:

返回类型为 IEnumerable 。用于处理LINQ to Entities操作远程数据源与本地集合的协作

十一、生成操作符

生成操作符返回一个新的集合。三个生成操作符不是扩展方法,而是返回序列的正常静态方法。

1) Empty:

生成一个具有指定类型参数的空序列 IEnumerable。

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); } //Adams, Terry //Andersen, Henriette Thaulow //Hedlund, Magnus //Ito, Shu //Solanki, Ajay //Hoeing, Helge //Potra, Cristina //Iallo, Lucio


2) Range:

生成指定范围内的整数的序列 IEnumerable。

如需要填充一二范围的数字,此时就应使用 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, 5).Select(n => n * 3);  foreach (var value in values) {      Console.WriteLine(value); } // 3 6 9 12 15


3) Repeat:

生成包含一个重复值的序列 IEnumerable。

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


IEnumerable<string> strings = Enumerable.Repeat("I like programming.", 3);  foreach (String str in strings) {     Console.WriteLine(str); } //I like programming. //I like programming. //I like programming.


十二、量词操作符

如果元素序列满足指定的条件,量词操作符就返回布尔值。

1) Any:

确定序列是否包含任何元素;或确定序列中的任何元素是否都满足条件。


//获取是否存在姓为“Schumacher”的车手冠军 var hasRacer_Schumacher = Formula1.GetChampions().Any(r => r.LastName == "Schumacher"); Console.WriteLine(hasRacer_Schumacher); //True


2) All:

确定序列中的所有元素是否满足条件。

3) Contains:

确定序列是否包含指定的元素。

十三、元素操作符

这些元素操作符仅返回一个元素,不是IEnumerable。(默认值:值类型默认为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);  Console.WriteLine(Racer3); //Ayrton Senna



十四、并行查询,并行Linq

AsParallel() 方法,扩展 IEnumerable 接口,返回 ParallelQuery类,所以正常的集合类可以以平行方式查询。


var query24 = from r in Formual.GetChampions().AsParallel() select r;


十五、分区器

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

手动创建一个分区器


var query25 = from r in Partitioner.Create (Formual.GetChampions(), true).AsParallel() select r;


十六、取消

.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!"); }