方法语法和查询语法
我们在写LINQ查询时可以使用两种形式的语法:查询语法和方法语法。
- 方法语法(methodsyntax)使用标准的方法调用。这些方法是一组叫作标准查询运算符的
方法,本章稍后会介绍。 - 查询语法(syntax)看上去和SQL语句很相似,使用查询表达式形式书写。
- 在一个查询中可以组合两种形式。
查询语法是声明式(declarafive)的,也就是说,查询描述的是你想返回的东西,但并没有
指明如何执行这个查询。方法语法是命令式(imperative)的,它指明了查询方法调用的顺序。
C#编译器会将使用查询语法表示的查询翻译为方法调用的形式。这两种形式在运行时没有性能上
的差异。
微软推荐使用查询语法,因为它更易读,能更清晰地表明查询意图,因此也更不容易出错。
然而,有一些运算符必须使用方法语法来书写。
如下代码演示了这两种形式以及它们的组合。对于方法语法的那部分代码,注意Where方法
的参数使用了Lambda表达式,第14章介绍过。本章后面还会介绍它在LINQ中的使用。
static void Main()
{
int[] numbers={2,5,28,31,17,16,42}
var numsQuery=from n in numbers //查询语法
where n<20
select n;
var numsMethod=numbers.where(N=>N<20) //方法语法
int numsCount=(from in numbers
where n<20
select n).Count(); //两种形式的组合
foreach(var x in numsQuery)
Console.Write($"{x}");
Console.WriteLine();
foreach(var x in numsMethod)
Console.Write($"{x}");
Console.WriteLine();
Console.WriteLine(numsCount);
}
查询变量
LINQ查询可以返回两种类型的结果一一可以是一个枚举,它是满足查询参数的项列表;也
可以是一个叫作标量(scalar)的单一值,它是满足查询条件的结果的某种摘要形式。
如下示例代码做了这些工作。
- 第一个语句创建了一个int数组并且使用3个值进行初始化。
- 第二个语句指定了一个LNQ查询,它可以用来枚举查询的结果。
- 第三个语句执行查询,然后调用一个LINQ方法(count)来返回从查询返回的项的总数。
稍后会介绍诸如count这样返回标量的运算符。
int[] numbers={2,5,28};
IEnumerable<int> lowNums=from n in numbers //返回一个枚举器
where n<20
select n;
int numCount=(from n in numbers //返回一个整数
where n<20
select n).Count();
第二条和第三条语句等号左边的变量叫作查询变量。尽管在示例的语句中显式定义了查询变
量的类型(IEnumerable和int),我们还是可以使用var关键字替代变量名称来让编译器自行
推断查询变量的类型。
理解查询变量的用法很重要。在执行前面的代码后,lowNums查洵变量不会包含查询的结果。
相反,编译器会创建能够执行这个查询的代码。
查询变量包含的是真实的整数值,它只能通过真实运行查询后获得。
查询执行时间的差异可以总结如下。
- 如果查询表达式返回枚举,则查询一直到处理枚举时才会执行。
- 如果枚举被处理多次,查询就会执行多次。
- 如果在进行遍历之后、查询执行之前数据有改动,则查询会使用新的数据。
- 如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中。
查询表达式的结构
如图20-2所示,查询表达式由from子句和查洵主体组成。有关查询表达式需要了解的
一些
重要事项如下。
- 子句必须按照一定的顺序出现。
- from子句和select…,group子句这两部分是必需的。
- 其他子句是可选的。
- 在LINQ查询表达式中,select子句在表达式最后。这与SQL的SELECT语句在查询的开
始处不一样。C#这么做的原因之一是让Visual Studio智能感应能在我们输人代码时提供
更多选项。
- 可以有任意多的from…let…where子句,如图20-2所示。
from子句
from子句指定了要作为数据源使用的数据集合。它还引入了迭代变量。有关from子句的要
点如下所示。
- 迭代变量逐个表示数据源的每一个元素。
- from子句的语法如下。
- Type是集合中元素的类型。这是可选的,因为编译器可以从集合中推断类型。
- ltem是迭代变量的名字。
- Item是要查的集合的名字。集合必须是可枚举的,见第19章。
如下的代码给出了用于查询由4个int构成的数组的查词表达式。迭代变量it会表示数组
中的每一个元素,并且会被之后的where和select子句选择或丢弃。这段代码没有指明迭代变量
的可选类型(int)。
int[] arr1={10,11,12,13};
var query=from item in arr1
where item<13 //使用迭代变量
select item; //使用迭代变量
foreach(var item in query)
Console.Write($"{item},");
图20-3演示了from子句的语法。类型说明符可以用关键字var,因为它可以由编译器推断。
可以有任意多个可选join子句。
尽管LINQ的from子句和foreach语句非常相似,但是主要的不同点如下。
-
foreach语句命令式地指定了要从第一个到最后一个按顺序访问集合中的项。而from子
句则声明式地规定集合中的每个项都要被访问,但并没有假定以什么样的顺序。 -
foreach语句在遇到代码时就执行其主体,而from子句什么也不执行。它创建可以执行
查询的后台代码对象。只有在程序的控制流遇到访问查询变量的语句时,才会执行查询。join子句
LINQ中的join子句和SQL中的JOIN子句很相似。如果你熟悉SQL中的联结,那么LINQ
中的联结对你来说应该不是新鲜事。不同的是,我们现在不但可以在数据库的表上执行联结,而
且还可以在集合对象上进行这个操作。如果你不熟悉联结或需要重新了解它,那么如下内容可能
会帮你理清思路。
需要先了解的有关联结的重要事项如下。
- 使用联结来结合两个或更多集合中的数据。
- 联结操作接受两个集合,然后创建一个临时的对象集合,其中每一个对象包含两个原始
集合对象中的所有字段。
联结的语法如下,它指定了第二个集合要和之前子句中的集合进行联结。注意必须使用上下
文关键字equals来比较字段,不能用==运算符。
join Type Identifier in Expression
on Expression equals Expression
join Type Identifier in Expression
on Expression equals Expression
into Identifier
如下具有注解的语句给出了一个join子句的示例:
什么是联结
LNQ中的join接受两个集合,然后创建一个新的集合,其中每一个元素包含两个原始集合
中的元素成员。
例如,如下的代码声明了两个类:Student和CourseStudent。
- Student类型的对象包含了学生的姓氏和学号。
- Coursestudent类型的对象表示参与课程的学生,它包含课程名以及学生的ID。
public class Student
{
public int StID;
public string LastName;
}
public class CourseStudent
{
public string CourseName;
public int StID;
}
图20-5演示了程序中的情况,在这里有3个学生和3门课程,学生参加了不同的课程。程
序有一个student对象构成的叫作students的数组,以及一个由CourseStudent对象构成的叫作
studentslncourses的数组,每一个学生参与的课程都包含一个对象。
假设我们现在希望获得某门课程中每个学生的姓氏。Students数组有姓氏,但不包含课程参
与信息。studentslncourses数组有参与课程的信息,但没有学生的名字。我们可以使用两个数
组中的对象都包含的学生ID号(StID)来将信息联系起来。可以通过在StID字段上进行联结来
实现。
如下的代码整合了整个示例,查询找出了所有选择历史课的学生的姓氏。
class Program
{
public class Student{
public int StID;
public string LastName;
}
public class CourseStudent{
public striing CourseName;
public int StID;
}
static Student[] students=new Student[]{
new Student{StID=1,LastName="Carson"},
new Student{StID=2,LastName="Klassen"},
new Student{StID=3,LastName="Fleming"},
};
static CourseStudent[] studentsInCourses=new CourseStudent[]{
new CourseStudent{CourseName="Art",StID=1},
new CousrseStudent{CourseName="Art",StID=2},
new CousrseStudent{CourseName="History,StID=1"},
new CouserStudent{CourseName="History",StID=3},
new CourseStudent{CourseName="Physics",StID=3},
};
static void Main()
{
//查找所有选择了历史课的学生的姓氏
var query=from s in students
join c in studentsInCourses on s.StID equals c.StID
where c.CourseName=="History"
select s.LastName;
//显示所有选择了历史课的学生的名字
foreach(var q in query)
Console.WriteLine($"Student taking History:{1}");
}
}
查询主体中的from…let…where片段
可选的from...let...where部分是查询主体的第一部分,可以由任意数量的3种子句构
成—from子句、let子句和where子句。图20-7总结了这些子句的语法。
### from子句
我们看到查询表达式从必需的from子句开始,后面跟的是查询主体。主体本身可以从任何
数量的其他行from子句开始,每一个from子句都指定了一个额外的源数据集合并引人了要在之后
运算的迭代变量。所有from子句的语法和含义都是一样的。
如下代码演示了这种用法的一个示例。
- 第一个from子句是查询表达式必需的子句。
- 第二个from子句是第一个子句的查询主体。
- select子句创建了一个匿名类型的对象。
static void Main()
{
var groupA=new[]{3,4,5,6}
var groupB=new []{6,7,8,9}
var someInts=from a in groupA //必须的第一个from子句
from b in groupB //查询主体的第一个子句
where a>4&& b<=8
select new {a,b,sum=a+b}; //匿名类型对象
foreach(var+in someInts)
Console.WriteLine(x);
}
let子句
let子句接受一个表达式的运算并且把它赋值给一个需要在其他运算中使用的标识符。let子句的语法如下:
let Identifier=Expression
例如,如下代码中的查询表达式将数组groupA中的每一个成员与数组groupB中的每一个成
员配对。where子句去除两个数组中相加不等于12的整数组合。
static void Main()
{
var grounA=new[]{3,4,5,6}
var grounB=new[]{6,7,8,9}
var someInts=from a in grounA
from b in grounB
let sum=a+b //在新的变量中保存结果
where sum==12
select new{a,b,sum};
foreach(var a in someInts)
Console.WriteLine(a);
}
where子句
where子句根据之后的运算来去除不符合指定条件的项。where子句的语法如下:
where BooleanExpression
有关需要了解的重要事项如下。
- 只要是在from.·.let...where部分中,查询表达式可以有任意多个where子句。
- 一个项必须满足所有where子句才能避免在之后被去除。
如下代码给出了一个包含两个where子句的查询表达式的示例。where子句去除了两个数组
中相加没有大于等于11的整数组合,以及groupA中元素值不等于4的项。选择的每一组元素必
定满足两个where子句条件。
static void Main()
{
var grounpA=new[]{3,4,5,6};
var groupB=new[]{6,7,8,9};
var someInts=from int a in grounpA
from int b in groupB
let sum=a+b
where sum>=11
where a==4
select new{a,b,sum};
foreach(var a in someInts)
Console.WriteLine(a);
}
orderby子句
orderby子句接受一个表达式并根据表达式按顺序返回结果项。
orderby子句的语法如图20-8所示。可选的ascending和descending关键字设置了排序
方向。表达式通常是项的一个字段。该字段不一定非得是数值字段,也可以是字符串这样的可
排序类型。
- orderby子句的默认排序是升序。然而,我们可以使用ascending和descending关键字显
式地设置元素的排序为升序或降序。
- 可以有任意多个子句,它们必须使用逗号分隔。
如下代码给出了一个按照学生年龄对学生记录进行排序的示例。注意,学生数据数组保存在
一个匿名类型数组中。
static void Main(){
var students=new[] //匿名类型的对象数组
{
new {LName="Jones",FName="Mary",Age=19,Major="History"},
new{LName="Smith",FName="Bob",Age=20,Major="CompSci"},
new{LName="Fleming",FName="Carol",Age=21,Major="History"}
};
var query=from student in students
orderby student.Age //根据年龄排序
select student;
foreach(var s in query)
{
Console.WriteLine($"{s.LName},{s.FName}:{s.Age},{s.Major}");
}
}
select…group子句
select…group部分由两种类型的子句组成一一一select子句和group…by子句。select…
group
部分之前的子句指定了数据源和要选择的对象,select…group部分的功能如下所示。
- select子句指定应该选择所选对象的哪些部分。它可以指定下面的任意一项。
- 整个数据项。
- 数据项的一个字段。
- 数据项中几个字段组成的新对象(或类似其他值)。
- group…by子句是可选的,用来指定选择的项如何被分组。本章稍后会介绍group…by
子句。
select…group子句的语法如图20-9所示。
如下代码给出了一个使用select子句选择整个数据项的示例。首先,我们创建了一个匿名
类型对象的数组;然后,查询表达式使用select语句来选择数组中的每一项。
using System;
using System.Linq;
class Program
{
static void Main()
{
var students=new[] //匿名类型的对象数组
{
new {LName="Jones",FName="Mary",Age=19,Major="History"},
new {LName="Smith",FName="Bob",Age=20,Major="CompSci"},
new {LName="Fleming",FName="Carol",Age=21,Major="History"}
};
var query=from s in students
select s;
foreach(var q in query)
Console.WriteLine($"{q.LName},{q.FName}:{q.Age},{q.Major}");
}
}
我们也可以使用select子句来选择对象的某些字段。例如,用下面的语句替代上面示例中
相应的语句,将只会选择学生的姓氏。
var query=from s in students
select s.LName;
foreach(var q in query)
Console.WriteLine(q);
查询中的匿名类型
查询结果可以由原始集合的项、原始集合中项的字段或匿名类型组成。
可以通过在select子句中把希望在类型中包括的字段以逗号分隔,并以大括号进行包围来
创建匿名类型。例如,要让前一节中的代码只选择学生姓名和主修课,可以使用如下的语法:
如下代码在select子句中创建一个匿名类型,稍后在WriteLine语句中使用。
using System;
using System.Linq;
class Program
{
static void Main()
{
var students=new[]
{
new{LName="Jones",FName="Mary",Age=19,Major="History"},
new{LName="Smith",FName="Bob",Age=20,Major="CompSci"},
new{LName="Fleming",FName="Carol",Age=21,Major="History"}
};
var query=from s in students
select new{s.LName,s.FName,s.Major};//创建匿名类型
foreach(var q in query)
Console.WriteLine($"{q.FName}{q.LName} -- {q.Major}");//匿名类型的访问字段
}
}
group子句
group子句根据指定的标准对选择的对象进行分组。例如,有了之前示例中的学生数组,程
序可以根据主修课程对学生进行分组。
有关group子句需要了解的重要事项如下。
- 如果项包含在查询的结果中,它们就可以根据某个字段的值进行分组。作为分组依据的
属性叫作键(key)。 - group子句返回的不是原始数据源中项的枚举,而是返回可以枚举已经形成的项的分组的
可枚举类型。 - 分组本身是可枚举类型,它们可以枚举实际的项。
group子句语法的一个示例如下。
例如,如下代码根据学生的主修课程进行分组:
static void Main()
{
var students=new[] //匿名类型的对象数组
{
new{LName="Jones",FName="Mary",Age=19,Major="History"},
new{LName="Smith",FName="Bob",Age=20,Major="CompSci"},
new{LName="Fleming",FName="Carol",Age=21,Major="History"}
};
var query=from student in students
group student by student.Major;
foreach(var g in query) //枚举分组
{
Console.WriteLine("{0}",g.Key);
foreach(var s in g) //枚举分组中的项
Console.WriteLine($" {s.LName},{s.FName}");
}
}
图20-10演示了从查询表达式返回并保存于查询变量中的对象。
- 从查询表达式返回的对象是从查询中枚举分组结果的可枚举类型。
- 每一个分组由一个叫作键的字段区分。
- 每一个分组本身是可枚举类型并且可以枚举它的项。
查询延续:into子句
查询延续子句可以接受查询的一部分的结果并赋予一个名字,从而可以在查询的另一部分中
使用。查询延续的语法如图20-11所示。
例如,如下查询联结了groupA和groupB,并将结果命名为groupAandB,然后从中进行一个
简单的select。
static void Main()
{
var groupA=new[] {3,4,5,6};
var groupB=new[] {4,5,6,7};
var someInts=from a in groupA
join b in groupB on a equals b
into groupAandB
from c in groupAandB
select c;
foreach(var v in someInts)
Console.Write($"{v}");
}