19.1 什么是LINQ
在关系型数据库系统中,可以通过 SQL 访问数据库中的任何数据。但在程序中,由于保存在类对象或结构中的数据差异很大,因此没有通用的查询语言来从数据结构中获取数据。从对象获取数据的方法一直是作为程序的一部分而设计的。然而 LINQ 可以很轻松地查询对象集合。
LINQ 高级特性:
- LINQ 代表 语言集成查询(Language Integrated Query)。
- LINQ 是 .NET 框架的扩张,它允许我们以使用 SQL 查询数据库的方式来查询数据集合。
- 使用 LINQ,可以从数据库、程序对象的集合以及 XML 文档中查询数据。
如下代码中,被查询的数据源是 int 数组,语句中查询的定义就是 from 和 select 关键字,尽管查询在语句中定义,但直到最后的 foreach 语句请求其结果的时候才会执行。
static void Main()
{
int[] numbers = { 2, 12, 5, 15 }; //数据源
IEnumerable<int> lowNums = //定义并存储查询
from n in numbers
where n < 10
select n;
foreach (var x in lowNums)
Console.Write("{0}, ", x); //执行查询
}
19.2 LINQ 提供程序
LINQ 可以和各种类型的数据源一起工作,比如 SQL 数据库、XML文档等,对于每一种数据源类型 ,在其背后一定有根据该数据源类型实现 LINQ 查询的代码模块。这些模块叫做 LINQ 提供程序(provider)。
本章着重介绍 LINQ 并解释如何将其用于程序对象(LINQ to Object)和XML(LINQ to XML)
匿名类型
无名类类型叫做匿名类型,经常用于 LINQ 查询的结果之中。
第六章介绍的对象初始化语句,允许我们在使用对象创建表达式时初始化新类实例的字段和属性。这种形式的对象创建表达式由三部分组成:
new关键字、类名、构造函数、对象初始化语句
对象初始化语句在一组大括号内包含了以逗号分隔的成员初始化列表。
创建匿名类型的变量使用相同的形式,但是没有类名和构造函数。如下代码演示了匿名类型的对象创建表达式:
下面代码给出了创建和使用匿名类型的实例。创建了一个 student 的变量,这是个有3个 string 属性和一个 int 属性的匿名类型。注意,在 WriteLine 中,可以像访问具名类型的成员那样访问实例的成员。
static void Main()
{
var student = new { Name = "Mary Jones", Age = 19, Major = "History" };
↑ ↑
必须使用var 匿名对象初始化语句
Console.WriteLine("{0}, Age {1}, Major: {2}", student.Name,student.Age,student.Major);
}
输出如下:
Mary Jones, Age 19, Major: History
- 匿名类型只能和局部变量配合使用,不能用于类成员
- 由于匿名类型没有名字,我们必须使用 var 关键字作为变量类型
- 不能设置匿名类型对象的属性。编译器为匿名类型创建的属性是只读的。
当编译器遇到匿名类型的对象初始化语句时,它创建了一个有名字的新类类型。对于每一个成员初始化语句,它推断其类型并创建一个只读属性来访问它的值。属性和成员初始化语句具有相同的名字。匿名类型被构造后,编译器创建了这个类型的对象。
除了对象初始化语句赋值形式,匿名类型的对象初始化语句还有其他两种形式:
简单标识符、成员访问表达式
这两种形式叫做 投影或映射初始化语句
下面例子展示了这三种初始化语句形式:
class Other
{
static public string Name = "Mary Jones";
}
class Program
{
static void Main(string[] args)
{
string Major = "History";
var student = new { Age = 19, Other.Name, Major };
↑ ↑ ↑
赋值形式 成员访问 标识符
Console.WriteLine("{0}, Age {1}, Major: {2}", student.Name, student.Age, student.Major);
}
}
输出结果与上例一致
上例演示的映射初始化语句形式和下面给出的赋值形式的结果是一样的:
var student = new { Age = Age, Name = Other.Name, Major = Major };
如果编译器遇到另一个具有相同的参数名、相同的推断类型和相同顺序的匿名类型,它会重用这个类型并直接创建新的实例,不会创建新的匿名类型。
19.3 方法语法和查询语法
可以使用两种语法进行LINQ查询:
查询语法和方法语法
- 方法语法(method syntax)使用标准的方法调用。这些方法是一组叫做标准查询运算符的方法
- 查询方法(query syntax)看上去和SQL语句很相似,使用查询表达式形式书写
- 一个查询中可以组合两种形式
查询语法是声明式的,即查询描述的是你想返回的东西,但并没有指明如何执行这个查询。
方法语法是命令式的,它指明了查询方法调用的顺序
两种语法在运行时没有性能上的差异。
微软推荐使用查询语法,更易读,更能清晰表明查询意图,也更不容易出错。但有些运算符必须使用方法语法来写。
下例演示了各种用法,其中方法语法中使用了Lambda表达式
static void Main(string[] args)
{
int[] numbers = { 2, 5, 28, 31, 17, 16, 42 };
var numsQuery = from n in numbers //查询语法
where n < 20
select n;
var numsMethod = numbers.Where(x => x < 20); //方法语法
int numsCount = (from n in numbers //两种组合
where n < 20
select n).Count();
foreach (var x in numsQuery)
{
Console.Write("{0}, ", x);
}
Console.WriteLine();
foreach (var x in numsMethod)
{
Console.Write("{0}, ", x);
}
Console.WriteLine();
Console.WriteLine(numsCount);
}
19.5 查询变量
LINQ 查询可以返回两种类型的结果:
枚举(可枚举的一组数据,不是枚举类型)
单一的标量值(满足查询条件的结果的某种摘要形式)
int [] number = { 2, 5, 28 };
IEnumerable<int> lowNums = from n in numbers //返回枚举数
where n < 20
select n;
int numsCount = (from n in numbers //返回一个整数
where n < 20
select n).Count();
第二条和第三条语句等号左边的变量叫查询变量。上例中显示定义了查询变量的类型(IEnumerable<T>和int),也可以用 var关键字替代变量名称让编译器自行判断查询变量的类型。
- 如果查询表达式返回枚举,查询一直到处理枚举时才会执行
- 如果枚举被处理多次,查询就会执行多次
- 如果在进行遍历之后,查询执行之前数据有改动,则查询会使用新的数据
- 如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中
19.5 查询表达式的结构
如图,查询表达式由查询体后的 from 子句组成
- 子句必须按照一定的顺序出现
- from 子句和 select...group 子句这两部分是必需的
- 其他子句是可选的
- 在 LINQ 查询表达式中,select子句在表达式最后。这与 SQL 的 SELECT 语句在查询的开始不同。C#这么做的原因之一是让VS智能感应能在我们输入代码时给我们更多选项。
-
可以有任意多的 from...let...where 子句
19.5.1 from子句
from 子句指定了要作为数据源使用的数据集合,它还引入了迭代变量
- 迭代变量逐个表示数据源的每一个元素
- from 子句的语法如下。
Type是集合中元素的类型。这是可选的,因为编译器可以从集合来推断类型。
Item是迭代变量的名字
Items是要查询的集合的名字。集合必须是可枚举的
如下代码,迭代变量 item 会表现数组中的每一个元素,并且会被之后的 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("{0}, ", item);
输出:
10, 11, 12,
下图演示了 from 子句的语法,类型说明符是可选的,可以由编译器推断。可以有任意多个 join 子句。
LINQ 的 from 子句和 foreach语句非常相似,区别如下:
- foreach 语句命令式地指定了要从第一个到最后一个按顺序地访问集合中地项。而 from 子句则声明式地规定集合中地每个项都要被访问,但并没有假定以什么样地顺序
- foreach 语句在遇到代码时就执行其主体,而 from 子句什么也不执行。它创建可以执行查询的后台代码对象。只有在程序的控制流遇到访问查询变量的语句时,才会执行查询
19.5.2 join子句
LINQ中的 join 子句与SQL中的类似,有关语法如下:
- 使用联结来结合两个或更多集合中的数据
- 联结操作接受两个集合然后创建一个临时的对象集合,每一个对象包含原始集合对象中的所有字段
联结的语法如下,指定了第二个集合和之前子句中的集合进行联结。注意必须使用上下文关键字 equals 来比较字段,不能用 == 运算符
下图演示了 join 子句的语法
join 子句示例:
19.5.3 什么是联结
LINQ 中的 join 接受两个集合然后创建一个新的集合,每一个元素包含两个原始集合中的原始成员。
例:
class Program
{
public class Student
{
public int StID; //声明类
public string LastName;
}
public class CourseStudent
{
public string 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[] studentInCourses = new CourseStudent[]
{
new CourseStudent { CourseName = "Art", StID = 1 },
new CourseStudent { CourseName = "Art", StID = 2 },
new CourseStudent { CourseName = "History", StID = 1 },
new CourseStudent { CourseName = "History", StID = 3 },
new CourseStudent { CourseName = "Art", StID = 3 },
};
static void Main(string[] args)
{
//查找所有选择了历史课的学生姓氏
var query = from t in students
join c in studentInCourses on t.StID equals c.StID
where c.CourseName == "History"
select t.LastName;
//显示所有选择了历史课的学生的名字
foreach (var q in query)
{
Console.WriteLine($"Student taking History: {q}");
}
}
}
输出如下:
Student taking History: Carson
Student taking History: Fleming
19.5.4 查询主体中的 from...let...where
let 子句
let 子句接受一个表达式的运算并且把它赋值给一个需要在其他运算中使用的标识符。let 子句语法如下:
let Identifier = Expression
例:
static void Main(string[] args)
{
var groupA = new[] { 3, 4, 5, 6};
var groupB = new[] { 6, 7, 8, 9};
var someInts = from w in groupA
from e in groupB
let sum = w + e
where sum == 12
select new { w, e, sum };
foreach (var d in someInts)
{
Console.WriteLine(d);
}
}
输出如下:
{ a = 3, b = 9, sum = 12 }
{ a = 4, b = 8, sum = 12 }
{ a = 5, b = 7, sum = 12 }
{ a = 6, b = 6, sum = 12 }
19.5.6 select...group子句
有两种类型的子句组成 select...group 部分----select 子句和 group...by 子句。select...group 部分之前的子句指定了数据源和要选择的对象。
- select 子句指定所选对象的哪部分应该被 select 。可指定下面任意一项
整个数据项
数据项的一个字段
数据项中几个字段组成的新对象(或其他类似值) - group...by子句是可选的,用来指定选择的项如何被分组。
select...group 子句语法如下
19.5.7 查询中的匿名类型
可以通过在 select 子句中把希望在类型中包括的字段以逗号隔开,并以大括号进行包围来创建匿名类型。
如下例:
var students = new[] //匿名类型的对象数组
{
new { LName = "Jones", FName = "Mary", Age = 19, Major = "History" },
new { LName = "Smith", FName = "Bob", Age = 19, 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}");//匿名类型的访问字段
}
输出如下
Mary Jones -- History
Bob Smith -- CompSci
Carol Fleming -- History
19.5.8 group子句
group 子句把 select 的对象根据一些标准进行分组。
- 如果项包含在查询的结果中,它们可以根据某个字段的值进行分组。作为分组依据的属性叫做键(key)。
- group 子句返回不是原始数据源中项的枚举,而是返回可以枚举已经形成的项的分组的可枚举类型。
- 分组本身是可枚举类型,它们可以枚举实际的项。
语法示例:
var students = new[] //匿名类型的对象数组
{
new { LName = "Jones", FName = "Mary", Age = 19, Major = "History" },
new { LName = "Smith", FName = "Bob", Age = 19, Major = "CompSci" },
new { LName = "Fleming", FName = "Carol", Age = 21, Major = "History" }
};
var query = from s in students
group s by s.Major;
foreach (var q in query) //枚举分组
{
Console.WriteLine($"{q.Key}");
↑
分组键
foreach (var t in q)
{
Console.WriteLine($" {t.LName}, {t.FName}");
}
}
输出如下:
History
Jones, Mary
Fleming, Carol
CompSci
Smith, Bob
下图演示了从查询表达式返回对象并保存于查询变量中
- 从查询表达式返回的对象是从查询中枚举分组结果的可枚举类型
- 每一个分组由一个叫做 键 的字段区分
- 每一个分组本身是可枚举类型并且可以枚举它的项
19.5.9 查询延续:into 子句
查询延续子句可以接受查询的一部分结果并赋予一个名字,从而可以在查询的另一部分中使用。语法如下:
例如下查询连接了 groupA 和 groupB,并命名为groupAandB,然后从groupAandB中进行一个简单的select。
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 a in someInts)
{
Console.Write($"{a} ");
}
输出如下:
4 5 6