9.3 LINQ查询操作

LINQ不仅提供了基本查询表达式,而且还提供了数十个查询操作,如筛选操作、投影操作、集合操作、聚合操作等。通过这些操作,可以更加方便、快捷地操作序列,并对序列实现筛选、投影、排序、聚合、联接等功能。本节将详细介绍LINQ查询操作及其使用方法。

9.3.1 查询操作概述

LINQ提供了数十个查询操作,大多数操作都在序列(实现了IEnumerable<T>或IQueryable<T>接口)之上运行。LINQ提供的查询操作可以简单地划分为筛选操作、投影操作、排序操作、聚合操作、集合操作、元素操作、数据类型转换操作、生成操作、限定符操作、数据分区操作、联接操作、相等操作、串联操作等。常用查询操作如表9-1所示。通过这些操作,可以对序列实现筛选、投影、排序、聚合、联接等功能。

figure_0355_0258

figure_0356_0259

figure_0357_0260

9.3.2 筛选操作

筛选操作Where能够处理由逻辑运算符(如逻辑“与”、逻辑“或”)组成的逻辑表达式,并从数据源中筛选数据,它和where子句的功能相似。示例代码如下所示:


int[]arr={1,2,3,4,5,6,7,8,9,10};

var result=arr.Where(i=>i>5&&i<10&&i!=7);

foreach(variin result)

{

Label1.Text+=i.ToString()+"<br>";

}


上述代码通过Where方法和Lambda表达式实现了对数据源中数据的筛选操作,其中Lambda表达式筛选了现有集合中所有值大于5、且小于10、且不等于7的元素并填充到新的集合中。当然,使用LINQ查询语句的子查询语句同样能够实现这样的功能。示例代码如下所示:


var result=from data in arr

where data>5&&data<10&&data!=7

select data;


上面的代码同样实现了LINQ中的筛选操作Where,但是使用筛选操作的代码更加简洁,其运行结果如图9-12所示。

figure_0357_0261

图 9-12 筛选操作示例运行结果

9.3.3 投影操作

在LINQ中,投影操作和SQL语句中的select子句功能相似,它能够选择数据源中的元素,并指定元素的表现形式。通常,投影操作包括以下两种操作:

❑Select操作:将数据源中的元素投影到新序列中,并指定元素的类型和表现形式。

❑SelectMany操作:也可以将数据源中的元素投影到新序列中,并指定元素的类型和表现形式。

但是,SelectMany操作可以将一个函数应用到多个序列之上,并将结果合并为一个序列。

1.Select操作

Select操作能够将数据源中的元素投影到新序列中,并指定元素的类型和表现形式,它和select子句的功能相似。Select操作将一个函数应用到一个序列之上,并产生另外一个序列。示例代码如下所示:


int[]arr={1,2,3,4,5,6,7,8,9,10};

var result=arr.Select(data=>data);

foreach(variin result)

{

Label1.Text+=i.ToString()+",";

}


上面的代码将集合中的元素进行投影并将符合条件的元素投影到新的集合中result去。由此可见,使用Select进行投影操作是非常简单的。

2.SelectMany操作

SelectMany操作和Select操作比较相似,它也可以将数据源中的元素投影到新序列中,并指定元素的类型和表现形式。但是,SelectMany操作可以将一个函数应用到多个序列之上,并将结果合并为一个序列。示例代码如下所示:


int[]arr1={1,2,3};

int[]arr2={4,5,6};

List<int[]>list=new List<int[]>();

list.Add(arr1);

list.Add(arr2);

var result=list.SelectMany(data=>data);

foreach(variin result)

{

Label1.Text+=i.ToString()+"<br>";

}


上面的代码通过SelectMany方法将不同的数据源投影到一个新的集合中,其运行结果如图9-13所示。

figure_0358_0262

图 9-13 SelectMany操作示例运行结果

9.3.4 排序操作

排序操作最常使用的是OrderBy方法,其使用方法同LINQ查询子句中的orderby子句基本类似。使用OrderBy方法能够对集合中的元素进行排序,同样OrderBy方法也能够针对多个关键字进行排序,可以按照一个或多个关键字对序列中的元素进行排序。其中,第一个排序关键字为主要关键字,第二个排序关键字为次要关键字。

除此之外,排序操作不仅提供了OrderBy方法,还提供了其他的方法进行高级排序。这些方法包括:

❑OrderBy操作:根据关键字对序列中的元素按升序排列。

❑OrderByDescending操作:根据关键字对序列中的元素按降序排列。

❑ThenBy操作:根据次要关键字对序列中的元素按升序排列。

❑ThenByDescending操作:根据次要关键字对序列中的元素按降序排列。

❑Reverse操作:将序列中的元素的顺序进行反转。

下面的示例演示了使用OrderBy对第一关键字Age进行升序排列,然后再使用ThenBy对第二关键字ID进行升序排列。如下面的代码所示:


List<Student>person=new List<Student>();

person.Add(new Student(1,25,"张三"));

person.Add(new Student(2,26,"张华"));

person.Add(new Student(3,25,"小西"));

person.Add(new Student(4,24,"张军"));

person.Add(new Student(5,26,"张雨"));

var result=person.OrderBy(p=>p.Age).ThenBy(p=>p.ID);

foreach(variin result)

{

Label1.Text+="年龄:"+i.Age.ToString()

+"——姓名:"+i.Name.ToString()

+"——编号:"+i.ID.ToString()+"<br>";

}


其运行结果如图9-14所示。

除此之外,还可以使用Reverse操作将这些排序的结果进行反转。示例代码如下所示:


var result=person.OrderBy(p=>p.Age).ThenBy(p=>p.ID).Reverse();

foreach(variin result)

{

Label1.Text+="年龄:"+i.Age.ToString()

+"——姓名:"+i.Name.ToString()

+"——编号:"+i.ID.ToString()+"<br>";

}


反转后的运行结果如图9-15所示。

figure_0359_0263

图 9-14 排序操作示例运行结果

figure_0359_0264

图 9-15 反转操作示例运行结果

9.3.5 聚合操作

有过SQL编程经验的读者知道,在SQL中往往需要统计一些基本信息,例如员工表里有多少员工、员工的平均年龄与工资、员工的总工资等。这些信息都可以通过SQL语句进行查询。在SQL查询语句中,支持一些能够进行基本运算的函数,这些函数包括Max、Min等。而在LINQ中,同样包括这些函数,用来获取集合中的最大值、最小值、平均值、总和等一些常用的统计信息,我们将这类操作统称为聚合操作。聚合操作常用的方法有:

❑Count操作:计算序列中元素的数量,或者计算序列满足一定条件的元素的数量。

❑Sum操作:计算序列中元素的总和。

❑Max操作:计算序列中元素的最大值。

❑Min操作:计算序列中元素的最小值。

❑Average操作:计算序列中元素的平均值。

❑Aggregate操作:对集合中的元素进行自定义的聚合计算。

❑LongCount操作:计算序列中元素的数量,或者计算序列满足一定条件的元素的数量。一般计算大型集合中的元素的数量。

下面的示例演示了这些聚合操作的使用方法。如下面的代码所示:


int[]arr={20,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};

var count=arr.Count(i=>i>13);

var sum=arr.Sum(i=>i);

var max=arr.Max(i=>i);

var min=arr.Min(i=>i);

var averagr=arr.Average(i=>i);

var aggregate=arr.Aggregate((x, y)=>x+y);

var longCount=arr.LongCount(i=>i>13);

Label1.Text+="Count:"+count.ToString()+"<br>";

Label1.Text+="Sum:"+sum.ToString()+"<br>";

Label1.Text+="Max:"+max.ToString()+"<br>";

Label1.Text+="Min:"+min.ToString()+"<br>";

Label1.Text+="Average:"+averagr.ToString()+"<br>";

Label1.Text+="Aggregate:"+aggregate.ToString()+"<br>";

Label1.Text+="LongCount:"+longCount.ToString()+"<br>";


运行结果如图9-16所示。

figure_0360_0265

图 9-16 聚合操作示例运行结果

9.3.6 集合操作

在LINQ中,集合操作是指对一个序列或多个序列本身的操作,如去掉重复元素、计算两个集合的交集等操作。它主要包括以下4种操作:

❑Distinct操作:可以去掉将数据源中重复的元素,并返回一个新序列。另外,它还可以指定一个比较器来比较两个元素是否相同。

❑Except操作:可以计算两个集合的差集(在一个集合中而不在另外一个集合中元素组成的集合)。

❑Intersect操作:可以计算两个集合的交集(由既在一个集合中,又在另外一个集合中的元素组成的集合)。

❑Union操作:可以计算两个集合的并集(由在一个集合中,或者在另外一个集合中的元素组成的集合)。

下面的示例演示了这些集合操作的使用方法。如下面的代码所示:


private void WriteLabel(List<string>list, string name)

{

Label1.Text+=name+"={";

for(int i=0;i<list.Count;i++)

{

if(i<list.Count-1)

{

Label1.Text+=list[i]+",";

}

else

{

Label1.Text+=list[i];

}

}

Label1.Text+="}<br/>";

}

protected void Page_Load(object sender, EventArgs e)

{

List<string>listA=new List<string>();

listA.Add("A");

listA.Add("A");

listA.Add("B");

listA.Add("C");

listA.Add("D");

listA.Add("D");

List<string>listB=new List<string>();

listB.Add("C");

listB.Add("D");

listB.Add("E");

listB.Add("F");

listB.Add("G");

var listADistinct=listA.Distinct();

WriteLabel(listA,"集合A");

WriteLabel(listB,"集合B");

WriteLabel(listADistinct.ToList<string>(),

"((Dstinct)去掉重复元素之后的集合A");

Label1.Text+="——<br/>";

var except=listA.Except(listB);

WriteLabel(except.ToList<string>(),"((Ecept)A与B差集");

Label1.Text+="——<br/>";

var intersect=listA.Intersect(listB);

WriteLabel(intersect.ToList<string>(),"((Itersect)A与B交集");

Label1.Text+="——<br/>";

var union=listA.Union(listB);

WriteLabel(union.ToList<string>(),"((Uion)A与B并集");

}


运行结果如图9-17所示。

9.3.7 元素操作

在LINQ中,元素操作可以获取计算序列中一个特定的元素。它包括以下8种操作:

❑ElementAt操作:返回集合中指定索引处的元素。

❑ElementAtOrDefault操作:返回集合中指定索引处的元素。如果索引超出集合的返回,则返回默认值。

figure_0362_0266

图 9-17 集合操作示例运行结果

❑First操作:返回集合的第一个元素,或者返回集合的满足指定条件的第一个元素。

❑FirstOrDefault操作:返回集合的第一个元素,或者返回集合的满足指定条件的第一个元素。如果不存在满足该条件的元素,则返回默认元素。

❑Last操作:返回集合的最后一个元素,或者返回集合的满足指定条件的最后一个元素。

❑LastOrDefault操作:返回集合的最后一个元素,或者返回集合的满足指定条件的最后一个元素。如果不存在满足该条件的元素,则返回默认元素。

❑Single操作:返回集合的唯一元素,或者返回集合的满足指定条件的唯一元素。

❑SingleOrDefault操作:返回集合的唯一元素,或者返回集合的满足指定条件的唯一元素。如果不存在满足该条件的元素,则返回默认元素。

下面的示例演示了这些元素操作的使用方法。如下面的代码所示:


int[]arr={1,2,2,4,5,5,7,8,9,0,3};

int elementAt=arr.ElementAt(8);

Label1.Text+="ElementAt:"+elementAt.ToString()+"<br>";

int elementAtOrDefault=arr.ElementAtOrDefault(16);

Label1.Text+="ElementAtOrDefault:"+elementAtOrDefault.ToString()+"<br>";

int first=arr.First();

Label1.Text+="First:"+first.ToString()+"<br>";

int last=arr.Last();

Label1.Text+="Last:"+last.ToString()+"<br>";


运行结果如图9-18所示。

figure_0362_0267

图 9-18 元素操作示例运行结果

9.3.8 数据类型转换操作

在LINQ中,数据类型转换操作可以将数据源的类型或者其元素的类型转换为用户指定的类型。它包括以下8种操作:

❑AsEnumerable操作:可以将数据源转换为IEnumerable<T>类型的序列。

❑AsQueryable操作:可以将数据源转换为IQueryable<T>或者IQueryable类型的序列。

❑Cast操作:将序列中的元素的类型转换为指定的类型(由TResult参数指定)。

❑OfType操作:从序列中筛选指定类型的元素,并构建为一个序列。

❑ToList操作:将IEnumerable<T>类型的序列转换为List<T>类型的序列。

❑ToArray操作:将IEnumerable<T>类型的序列转换为T[]类型的数组。

❑ToDictionary操作:按照键值将序列中的元素放入一对一的字典序列((Dctionary<TKey, TValue>)中。

❑ToLookup操作:按照键值将序列中的元素放入一对多的字典序列((Lokup<TKey, TValue>)中。

9.3.9 生成操作

在LINQ中,生成操作能够产生指定的新序列。它常包括以下4种操作:

❑DefaultIfEmpty操作:返回IEnumerable<T>类型的序列。如果序列为空,则返回只包含一个元素(值为默认值或者指定的值)的序列。

❑Empty操作:返回IEnumerable<T>类型的空序列。

❑Range操作:返回指定范围的数字序列。

❑Repeat操作:返回IEnumerable<T>类型的包含一个重复值的序列。

下面的示例演示了DefaultIfEmpty操作的使用方法。如下面的代码所示:


int[]arr={1,2,2,4,5,5,7,8,9,0,3};

int[]newarr={};

var values=arr.DefaultIfEmpty();

var newvalues=newarr.DefaultIfEmpty(-1);

foreach(varvin values)

{

Label1.Text+=v+",";

}

Label1.Text+="<br/>";

foreach(varvin newvalues)

{

Label1.Text+=v+",";

}


运行结果如图9-19所示。

figure_0363_0268

图 9-19 DefaultIfEmpty操作示例运行结果

9.3.10 限定符操作

在LINQ中,限定符操作可以检测序列中是否存在满足指定条件的元素,或者检测序列中的所有元素满足指定的条件,它返回一个布尔值。它包含以下3种操作:

❑All操作:检测序列中的所有元素是否都满足指定的条件。如果满足,则返回true,否则返回false。

❑Any操作:检测序列中是否存在满足指定条件的元素。如果存在这样的元素,则返回true,否则返回false。

❑Contains操作:检测序列中是否存在指定的元素。如果存在这样的元素,则返回true,否则返回false。

9.3.11 连接操作

LINQ只提供了两种连接操作:Join和GroupJoin。这两种连接都属于相等连接,即根据两个数据源的键是否相等来匹配这两个数据源的连接。

1.Join连接

它要求元素的连接关系必须同时满足被连接的两个数据源,和SQL语句中的inner join子句相似。下面的示例代码演示了这种连接方式:


List<Student>person=new List<Student>();

person.Add(new Student(1,25,"张三"));

person.Add(new Student(2,26,"张华"));

person.Add(new Student(3,25,"小西"));

person.Add(new Student(4,24,"张军"));

person.Add(new Student(5,26,"张雨"));

List<Course>course=new List<Course>();

course.Add(new Course(1,"ASP.NET"));

course.Add(new Course(2,"C"));

course.Add(new Course(3,"VB"));

var result=person.Join(course,

p=>p.ID,

c=>c.ID,

(p, c)=>new{Name=p.Name, CourseName=c.CourseName});

foreach(varvin result)

{

Label1.Text+=v.Name+":"+v.CourseName+"<br/>";

}


运行结果如图9-20所示。

figure_0364_0269

图 9-20 Join操作示例运行结果

2.GroupJoin连接

它产生分层数据结构,将第一个集合中的每个元素与第二个集合中的一组相关元素进行匹配。在查询结果中,第一个集合中的元素都会出现在查询结果中。如果第一个集合中的元素在第二个集合中找到相关元素,则使用被找到的元素,否则使用空。下面的示例代码演示了这种连接方式:


List<Student>person=new List<Student>();

person.Add(new Student(1,25,"张三"));

person.Add(new Student(2,26,"张华"));

person.Add(new Student(3,25,"小西"));

person.Add(new Student(4,24,"张军"));

person.Add(new Student(5,26,"张雨"));

List<Course>course=new List<Course>();

course.Add(new Course(1,"ASP.NET"));

course.Add(new Course(2,"C"));

course.Add(new Course(3,"VB"));

var result=person.GroupJoin(course,

p=>p.ID,

c=>c.ID,

(p, c)=>new{Name=p.Name, Courses=c.ToList()});

foreach(varvin result)

{

Label1.Text+=v.Name+":"

+(v.Courses.Count>0?v.Courses[0].CourseName:"没有选课")

+"</br>";

}


运行结果如图9-21所示。

figure_0365_0270

图 9-21 GroupJoin操作示例运行结果

9.3.12 SequenceEqual操作

SequenceEqual操作可以判断两个序列是否相等,它返回一个布尔值。即给定两个序列,如果这两个序列相等,则必须满足以下两个条件:

1)序列元素的数量相等,即序列的长度相等。

2)两个序列的对应元素相等。

如下面的示例代码所示:


string[]arr1={"AB","BC","CD"};

string[]arr2={"DE","EF","FG"};

bool result=arr1.SequenceEqual(arr2);


上面的结果返回false。

9.3.13 Contact操作

Contact操作可以实现序列的串联操作,即将一个序列的元素全部追加到另一个序列中,并构成一个新的序列。如下面的示例代码所示:


string[]arr1={"AB","BC","CD"};

string[]arr2={"DE","EF","FG"};

var result=arr1.Concat(arr2);

foreach(varvin result)

{

Label1.Text+=v+",";

}


运行结果如图9-22所示。

figure_0365_0271

图 9-22 Contact操作示例运行结果

9.3.14 Skip与SkipWhile操作

Skip操作可以生成一个新序列,它可以跳过数据源(序列)中指定数量的元素,然后返回由数据源(序列)剩余的元素组成的序列。示例如下面的代码所示:


int[]arr={0,1,2,3,4,5,6,7,8,9};

var result=arr.Skip(5);

foreach(variin result)

{

Label1.Text+=i+",";

}


上面的代码输出结果为:5,6,7,8,9。

与Skip操作一样,SkipWhile操作也将生成一个新序列。它可以跳过数据源(序列)中满足指定条件的元素,然后返回由数据源(序列)剩余的元素组成的序列。示例如下面的代码所示:


int[]arr={0,1,2,3,4,5,6,7,8,9};

var result=arr.SkipWhile((x, i)=>i<5);

foreach(variin result)

{

Label1.Text+=i+",";

}


同样,代码的输出结果为:5,6,7,8,9。

9.3.15 Take与TakeWhile操作

Take操作可以生成一个新序列,但它和Skip操作相反。它从数据源(序列)的开头开始提取指定数量的元素,然后返回由这些元素组成的序列。示例如下面的代码所示:


int[]arr={0,1,2,3,4,5,6,7,8,9};

var result=arr.Take(5);

foreach(variin result)

{

Label1.Text+=i+",";

}


上面的代码输出结果为:0,1,2,3,4。

与Take操作一样,TakeWhile操作也可以生成一个新序列。不同的是,TakeWhile操作可以从数据源(序列)的开头开始提取满足指定条件的元素,然后返回由这些元素组成的序列。示例如下面的代码所示:


int[]arr={0,1,2,3,4,5,6,7,8,9};

var result=arr.TakeWhile((x, i)=>i<5);

foreach(variin result)

{

Label1.Text+=i+",";

}


同样,代码的输出结果为:0,1,2,3,4。