1.
简介
query ( 查询 ) 是一种从数据源检索数据的表达式。查询一般用专门的查询语言来实现。对各种数据源,都 已经有对应的的查询语言,例如,用于关系数据库的 SQL 语言,用于 XML 的 XQuery 语言。因此,开发人员不得不对他们必须支持的每种数据源或数据格式学习新的查询语言。 LINQ 为了简化这一情况,提供了一种跨各种数据源和数据格式的的模型。在 LINQ 查询中,面对的始终是对象。你可以使用相同的编码模式来查询和转换 XML 文档、 SQL 数据库、 ADO.NET 数据集、 .NET 集合以及其他 LINQ 提供的文件格式。
query ( 查询 ) 是一种从数据源检索数据的表达式。查询一般用专门的查询语言来实现。对各种数据源,都 已经有对应的的查询语言,例如,用于关系数据库的 SQL 语言,用于 XML 的 XQuery 语言。因此,开发人员不得不对他们必须支持的每种数据源或数据格式学习新的查询语言。 LINQ 为了简化这一情况,提供了一种跨各种数据源和数据格式的的模型。在 LINQ 查询中,面对的始终是对象。你可以使用相同的编码模式来查询和转换 XML 文档、 SQL 数据库、 ADO.NET 数据集、 .NET 集合以及其他 LINQ 提供的文件格式。
1.1 三步查询
所有 LINQ 查询都包含三步:
- 获得数据源
- 创建查询(query)
- 执行查询(query)
下面演示如何在代码中表示这三个部分。该例子使用 integer 数组作为数据源。
class IntroToLINQ {
static void Main () {
// LINQ 查询的 3 部分 :
// 1. 数据源
int [] numbers = new int [7] { 0, 1, 2, 3, 4, 5, 6 };
// 2. 创建查询
// numQuery is an IEnumerable<int>
var numQuery =
from num in numbers
where ( num % 2) == 0
select num ;
// 3. 执行查询
foreach ( int num in numQuery ) {
Console . Write ( " {0,1} " , num );
}
}
}
下图显示了完整的查询操作。在 LINQ 中,查询的执行与查询本身是不同的;换句话说,创建查询变量时,是没有执行检索操作的。
1.2 数据源
在上例中,数据源是数组,它隐式支持 IEnumerable<T> 接口,所以可以用 LINQ 进行查询。查询在 foreach 语句中执行,而 foreach 循环要求对象 IEnumerable 或 IEnumerable<T> 。支持 IEnumerable<T> 或其派生接口 ( 如 IQueryable<T> 的类型称为可查询类型 (queryable types) 。
可查询类型可直接作为 LINQ 数据源。如果源数据不以可查询类型存在,则 LINQ 提供者应该以可查询类型的形式提供。例如, LINQ-XML 将 XML 文档加载到可查询类型 XElement 中:
// 从 XML 文档创建数据源
// using System.Xml.Linq;
XElement contacts = XElement . Load ( @"c:\myContactList.xml" );
在 SQL-LINQ 中,必须首先手动或使用对象关系设计器 (Object Relational Designer O/R-Designer) 创建对象关系映射 (object-relational mapping) 。针对这些对象编写查询,运行时由 SQL-LINQ 处理与数据库的通信。在下例中, Customers 表示数据库中一个特定表格,查询结果为派生自 IEnumerable<T> 的 IQueryable<T> 类型。
private Northwnd db = new Northwnd ( @"c:\northwnd.mdf" );
// 查询在 London 的 customers
private IQueryable < Customer > custQuery =
from cust in db . Customers
where cust. City == "London"
select cust;
有关如何创建特定类型的数据源,可以参考 LINQ providers 。但基本规则都是一样: LINQ 数据源是支持泛型接口 IEnumerable<T> 的任意对象 。
1.3 查询 (Query)
查询指定要从数据源中检索的信息。查询还可以指定在返回结果前如何对信息进行排序、分组和结构化。查询存储在查询变量中,并通过查询表达式进行初始化。为使编写查询更为简单, C# 引入了新的查询语法。
第一个例子从数组中返回所有的偶数。查询表达式包含三个子句: from , where 和 select 。 ( 如果你熟悉 SQL ,你会注意到这些子句的顺序与 SQL 中的正好相反 ) 。 from 子句指定数据源, where 子句应用筛选器, select 子句指定返回元素的类型。需要注意到的是, LINQ 中,查询变量本身不执行任何操作,且不返回任何数据。它只是存储以后某个时刻执行查询生成结果所需的信息。
1.4 执行查询 (Query Execution)
延迟执行 (Deferred Execution)
如前所述,查询变量只是存储查询命令。而实际的查询会延迟到在 foreach 语句中循环访问查询变量时执行。此概念称为 " 延迟执行 "(deferred execution) ,如下所示 :
// Query execution.
foreach ( int num in numQuery )
{
Console . Write ( " {0,1} " , num );
}
foreach 语句是检索查询结果的地方。例如,在上例中,迭代变量 num 保存了返回序列中的每个值。
因为查询变量本身不保存查询结果,因为可以根据需要随意执行查询。例如,可以通过一个程序持续更新数据库。在另一个程序中,可以创建一个检索最新数据的查询,然后每隔一段时间执行该查询以便每次检索不同的结果。
强制立即执行 (Forcing Immediate Execution)
对一系列源数据执行聚合函数 (aggregation function) 查询必须首先循环访问这些元素。 Count, Max, Average 和 First 就属于这类查询。这些查询隐式使用 foreach 语句返回结果。另外也要注意,这些类型的查询返回单个值,而不是 IEnumerable 集合。下面的查询返回源数组中偶数的个数:
var evenNumQuery =
from num in numbers
where ( num %2) == 0
select num ;
int evenNumCount = evenNumQuery . Count ();
若要强制立即执行查询并缓存结果,可以调用 ToList<TSource> 或 ToArray<TSource> 方法,如下:
List < int > numQuery2 =
( from num in numbers
where (num % 2) == 0
select num). ToList ();
// 或者这样 :
// numQuery3 为 int[]
var numQuery3 =
( from num in numbers
where (num % 2) == 0
select num). ToArray ();
此外,也可以通过在查询表达式后放置一个 foreach 循环来强制执行查询。通过调用 ToList 或 ToArray 同样将所有数据缓存在单个集合对象中。
2.LINQ 和泛型类型
基于泛型类型的 LINQ 在 .NET Framework 2.0 版本中引入。首先,你需要了解两个基本概念:
- 在创建泛型集合类(如List<T>)的实例时,将 "T"替换为列表中将包含的对象的类型。例如,字符串列表表示为List<string>,Customer对象列表表示为 List<Customer>。泛型列表是强类型,相对于以Object的形式存储数据有许多有点。如果将 Customer添加到 List<string>,则会引起编译错误。
- 通过IEnumerable<T>接口可以使用foreach语句枚举泛型集合类。泛型集合类和非泛型集合(如ArrayList)一样支持 IEnumerbale<T>。
2.1 LINQ 查询中的 IEnumerable<T> 变量
LINQ 查询变量为 IEnumerable<T> 类型或其派生类型。当你看到类型为 INumerable<Customer> 的查询变量时,说明执行该查询会生成 Customer 对象序列。
IEnumerable < Customer > customerQuery =
from cust in customers
where cust. City == "London"
select cust;
foreach ( Customer customer in customerQuery ) {
Console . WriteLine ( customer . LastName + ", " + customer . FirstName );
}
2.2 让编译器处理泛型类声明
完全可以使用 var 关键字代替泛型语句。 var 关键字告诉编译器,通过查看 from 子句中指定的数据源来推断查询变量的类型。下面的例子生成与上一个例子完全相同的编译代码:
var customerQuery2 =
from cust in customers
where cust . City == "London"
select cust ;
foreach ( var customer in customerQuery2 ) {
Console . WriteLine ( customer . LastName + ", " + customer . FirstName );
}
3.LINQ 基本查询操作
简要介绍 LINQ 查询表达式,以及在查询中用到的一些典型操作。
3.1 获得数据源
在 LINQ 查询中,第一步就是指定数据源:使用 from 子句引入数据源 (customers) 和范围变量 (cust) ,如下:
//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
select cust ;
范围变量类似于 foreach 循环中的迭代变量,不过在循环表达式中没有迭代。执行查询时,范围变量 (cust) 对 customers 中的元素进行引用。因为编译器可以推断 cust 的类型,所以你不必显式指定其类型。还可以用 let 子句引入其他范围变量。具体查看 let 子句。
NOTE :对非泛型数据源 ( 如 ArrayList) ,范围变量必须显式指定其类型。
3.2 筛选
筛选器可能是最常见的查询操作了。使用筛选器使查询只返回那些表达式结果为 true 的元素。通过 where 子句指定筛选器。筛选器指定从数据源中排除哪些元素。在下面的示例中,过滤掉所有地址不为 London 的 customers 。
var queryLondonCustomers = from cust in customers
where cust . City == "London"
select cust ;
还可以在 where 子句中通过 C# 逻辑 AND 和 OR 运算符指定多个筛选器。如下,只返回 "London" 且名为 "Devon" 的 customers:
where cust . City == "London" && cust . Name == "Devon"
返回来自 London 或 "Paris" 的 customers:
where cust .City == "London" || cust .City == "Paris"
3.3 排序
使用 orderby 子句对返回的数据进行排序。例如,下面以 Name 属性对结果进行排序。因为 Name 为字符串,所以默认以字母顺序排序:
var queryLondonCustomers3 =
from cust in customers
where cust . City == "London"
orderby cust . Name ascending
select cust ;
若要以相反顺序排序,可以使用 orderby … descending 子句。
3.4 分组
使用 group 子句可以按照指定的键值对结果进行分组。例如,可以按照 City 对 customers 进行分组,从而让来自 London 和 Paris 处于不同组:
// queryCustomersByCity is an IEnumerable<IGrouping<string, Customer>>
var queryCustomersByCity =
from cust in customers
group cust by cust . City ;
// customerGroup is an IGrouping<string, Customer>
foreach ( var customerGroup in queryCustomersByCity ) {
Console . WriteLine ( customerGroup . Key );
foreach ( Customer customer in customerGroup ) {
Console . WriteLine ( " {0} " , customer . Name );
}
}
在使用 group 子句查询后,结果以 list of lists 的形式存储。因此在访问生成组序列的查询时,可以使用嵌套 foreach 循环。
如果你必须引用组查询操作的结果,可以使用 into 关键字创建可供进一步查询的标识符。例如,下面只返回组成员个数大于 2 的的组:
// custQuery is an IEnumerable<IGrouping<string, Customer>>
var custQuery =
from cust in customers
group cust by cust . City into custGroup
where custGroup . Count () > 2
orderby custGroup . Key
select custGroup ;
3.5 联接
联接操作为数据源中没有显式建模的序列之间建立关联。例如,可以执行联接操作来查找位于同一地点的所有客户和经销商。在 LINQ 中, 使用 join 子句对集合对象进行操作。
var innerJoinQuery =
from cust in customers
join dist in distributors on cust . City equals dist . City
select new {CustomerName = cust . Name , DistributorName = dist . Name };
在 LINQ 中,不必像在 SQL 中那样频繁使用 join ,因为 LINQ 中的 foreign keys 在对象模型中以包含项目集合的属性表示。例如, Customer 对象包含 Order 对象的集合。不必执行联接,直接通过点运算符访问 orders :
from order in Customer .Orders...
3.6 Selecting
select 子句用于指定查询结果值的类型。该子句的结果取决于前面计算的结果以及 select 自己本身的表达式。查询表达式必须以 select 或 group 子句结束。
下面示例简单的 select 子句。
class SelectSample1 {
static void Main () {
//Create the data source
List < int > Scores = new List < int >() { 97, 92, 81, 60 };
// Create the query.
IEnumerable < int > queryHighScores =
from score in Scores
where score > 80
select score;
// Execute the query.
foreach ( int i in queryHighScores ) {
Console . Write ( i + " " );
}
}
}
//Output: 97 92 81
select 子句产生序列的类型决定了查询变量 queryHighScores 的类型。最简单的情况是, select 子句仅指定范围变量。此时返回的序列包含的元素类型和源数据类型一致。
4. 使用 LINQ 进行数据转换
LINQ 不仅可用于数据检索,还是一个十分强大的数据转换工具。通过 LINQ 查询,将源序列用作输入,以需要的方式修改后创建一个新的输出序列。可以是过滤、排序或分组等不会修改元素的方式修改序列,也可以创建新的类型。该功能在 select 子句中实现。具体可执行的功能包括:
- 将多个输入序列合并成一个新的输出序列。
- 创建只包含输入序列的某些属性的输出序列。
- 对源数据进行处理后,其结果作为输出序列。
- 以不同格式输出。例如,可以将SQL行或者文本文件转化为XML文件。
4.1 合并多个输入为一个输出
下例演示如何合并两个数据结构,合并 XML 或 SQL 数据的方式类似,首先给出两个数据类型:
class Student {
public string First { get ; set ; }
public string Last { get ; set ; }
public int ID { get ; set ; }
public string Street { get ; set ; }
public string City { get ; set ; }
public List < int > Scores ;
}
class Teacher {
public string First { get ; set ; }
public string Last { get ; set ; }
public int ID { get ; set ; }
public string City { get ; set ; }
}
下面是执行查询合并:
class DataTransformations {
private static void Main (){
// Create the first data source.
List < Student > students = new List < Student >(){
new Student {
First = "Svetlana" ,
Last = "Omelchenko" ,
ID = 111,
Street = "123 Main Street" ,
City = "Seattle" ,
Scores = new List < int > {97, 92, 81, 60}
},
new Student {
First = "Claire" ,
Last = "O’Donnell" ,
ID = 112,
Street = "124 Main Street" ,
City = "Redmond" ,
Scores = new List < int > {75, 84, 91, 39}
},
new Student {
First = "Sven" ,
Last = "Mortensen" ,
ID = 113,
Street = "125 Main Street" ,
City = "Lake City" ,
Scores = new List < int > {88, 94, 65, 91}
},
};
// Create the second data source.
List < Teacher > teachers = new List < Teacher >()
{
new Teacher { First = "Ann" , Last = "Beebe" , ID = 945, City = "Seattle" },
new Teacher { First = "Alex" , Last = "Robinson" , ID = 956, City = "Redmond" },
new Teacher { First = "Michiyo" , Last = "Sato" , ID = 972, City = "Tacoma" }
};
// Create the query.
var peopleInSeattle = ( from student in students
where student. City == "Seattle"
select student. Last )
. Concat ( from teacher in teachers
where teacher. City == "Seattle"
select teacher. Last );
Console . WriteLine ( "The following students and teachers live in Seattle:" );
// Execute the query.
foreach ( var person in peopleInSeattle )
{
Console . WriteLine ( person );
}
Console . WriteLine ( "Press any key to exit." );
Console . ReadKey ();
}
}
/* 输出 :
The following students and teachers live in Seattle:
Omelchenko
Beebe
*/
4.2 选择子集
选择源数据中元素的子集的方法有两种:
- 若只选择一个成员,则使用点运算即可。如下,假定Customer对象包含几个公共属性,其中包括名为City的字符串:
select cust . City ;
- 若要选择多个多个属性,可以使用命名对象或匿名对象。如下,使用匿名类型封装各个Customer元素的两个属性:
select new { Name = cust . Name , City = cust . City };
4.3 将对象转换为 XML
通过 LINQ 查询,可以轻松地将数据结构、 SQL 数据库、 ADO.NET 数据集和 XML 流或文档互相转换。下面演示如何将数据结构转换为 XML 元素。
class XMLTransform {
static void Main () {
// Create the data source by using a collection initializer.
// The Student class was defined previously in this topic.
List < Student > students = new List < Student >()
{
new Student { First = "Svetlana" , Last = "Omelchenko" , ID =111, Scores = new List < int >{97, 92, 81, 60}},
new Student { First = "Claire" , Last = "O’Donnell" , ID =112, Scores = new List < int >{75, 84, 91, 39}},
new Student { First = "Sven" , Last = "Mortensen" , ID =113, Scores = new List < int >{88, 94, 65, 91}},
};
// Create the query.
var studentsToXML = new XElement ( "Root" ,
from student in students
let x = String . Format ( " {0} , {1} , {2} , {3} " , student. Scores [0],
student. Scores [1], student. Scores [2], student. Scores [3])
select new XElement ( "student" ,
new XElement ( "First" , student . First ),
new XElement ( "Last" , student . Last ),
new XElement ( "Scores" , x )
) // end "student"
); // end "Root"
// Execute the query.
Console . WriteLine ( studentsToXML );
// Keep the console open in debug mode.
Console . WriteLine ( "Press any key to exit." );
Console . ReadKey ();
}
}
生成如下的 XML
< Root >
< student >
< First > Svetlana </ First >
< Last > Omelchenko </ Last >
< Scores > 97,92,81,60 </ Scores >
</ student >
< student >
< First > Claire </ First >
< Last > O'Donnell </ Last >
< Scores > 75,84,91,39 </ Scores >
</ student >
< student >
< First > Sven </ First >
< Last > Mortensen </ Last >
< Scores > 88,94,65,91 </ Scores >
</ student >
</ Root >
5.LINQ 查询中的类型关系
到现在,可能你还对 LINQ 查询中数据类型的变化不大确定。 LINQ 查询在数据源、查询本身及查询执行中,都是强类型的。查询变量类型必须与数据源中元素的类型和 foreach 语句中迭代变量的类型兼容。为了演示这些类型关系,下面的大多数示例都使用显式类型。
5.1 不转换源数据的查询
下图演示不对数据执行转换的 LINQ-Objects 查询。数据源包含一个字符串列表,输出也是:
- 源数据的类型参数决定了范围变量的类型,所以name为 string类型。
- 选择对象类型决定查询变量类型。name为一个字符串,所以查询变量nameQuery为IEnumerable<string>类型。
- 在foreach语句中循环访问查询变量。因为查询变量为IEnumerable<string>类型,所以迭代变量str为string类型。
5.2 转换源数据的查询
下图演示对数据进行简单转换的 LINQ-SQL 查询。将一个 Customer 对象列表作为输入,并只选择结果中的 Name 属性。因为 Name 是字符串,所以查询生成的结果为字符串序列。
- 数据源的类型Table<Customer>决定范围变量cust类型为 Customer
- select子句返回Name属性,而非完整的Customer对象,Name为字符串,所以custNameQuery的类型参数为string。
- custNameQuery决定foreach循环中的迭代变量必须是string类型。
下面演示一个稍微复杂的转换。 select 子句返回捕获 Customer 对象的两个成员的匿名类型
- 数据源类型决定cust类型为Customer
- 因为select语句声明匿名类型,所以必须使用var隐式类型化查询变量
- 因为查询变量的类型是隐式的,所以foreach循环中的迭代变量也必须是隐式的。
5.3 让编译器推断类型信息
虽然了解查询中的类型关系是必须的,但也可以选择让编译器执行全部工作。关键字 var 用于查询操作中的任何局部变量。由编译器为各个变量提供强类型。
6 LINQ 中的查询语法和方法语法
大多数 LINQ 查询都使用声明式的查询语法。当代码编译后,会被转化为 .NET CLR 的方法调用。这些方法调用的标准查询运算符,如 Where, Select, GroupBy, Join, Max 和 Average 等。
查询语法和方法语法,语义是一致的。不过查询语法更简单、更容易阅读。但是有些查询必须表示为方法调用的形式。例如,查找满足指定条件元素的数量的查询,必须使用方法调用形式。查询数据最大值时也需要使用方法调用。 System.Linq 命名空间里的标准查询运算符一般使用方法语法。所以,熟悉方法语法是很有必要的。
6.1 标准查询运算符扩展方法
下面演示简单的查询表达式和等效的基于方法的查询:
class QueryVMethodSyntax {
static void Main () {
int [] numbers = { 5, 10, 8, 3, 6, 12 };
// 查询语法 :
IEnumerable < int > numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
// 方法语法 :
IEnumerable < int > numQuery2 = numbers . Where (num => num % 2 == 0). OrderBy (n => n);
foreach ( int i in numQuery1 ) {
Console . Write ( i + " " );
}
Console . WriteLine ( System . Environment . NewLine );
foreach ( int i in numQuery2 ) {
Console . Write ( i + " " );
}
// Keep the console open in debug mode.
Console . WriteLine ( System . Environment . NewLine );
Console . WriteLine ( "Press any key to exit" );
Console . ReadKey ();
}
}
/*
输出 :
6 8 10 12
6 8 10 12
*/
两个示例的输出是相同的,两种形式的查询变量类型也相同: IEnumerable<T> 。
在表达式的右侧, where 子句现在表示为对 numbers 对象的实例方法, numbers 的类型为 IEnumerable<int> ,查看 IEnumerable<T> API ,你可以看到它没有 Where 方法。但是在 VS 的 IntelliSense 完成列表中,您不仅能看到 Where 方法,而且还会看到 Select, SelectMany, Join 和 OrderBy 等,如下所示:
这些标准查询运算符都是以扩展方法实现的。扩展方法可 " 扩展 " 现有类型,可如对类型的实例方法一样调动这些扩展方法。
6.2 Lambda 表达式
在上面的示例中,注意到条件表达式 (num%2 == 0) 是以内联参数的形式传递给 Where 方法: Wherer (num => num%2 == 0) 。此内联表达式称为 Lambda 表达式。将代码写为匿名方法、泛型委托或表达式树是一种便捷的方法。在 C# 中, => 为 Lambda 运算符,可读作 "goes to" 。运算符左侧的 num 是输入变量,和查询表达式中的 num 对应。编译器可自动推断 num 的类型。 lambda 表达式与查询语法中的表达式或任何其他 C# 表达式相同,它可以包括方法调用和其他复杂逻辑。其返回值为表达式结果。
初步使用 LINQ , lambda 不是必须的。但是,有些特定查询只能以方法语法表示,而其中一些必须要用到 lambda 表达式。所以,熟悉 lambda 表达式是灵活 LINQ 所必须的。
6.3 查询的组合性
注意到,在上面的代码中 OrderBy 是通过调用 Where 之后来调用的。 Where 生成筛选序列,然后 OrderBy 对该序列排序。因为查询会返回 IEnumerable ,所以可将方法链接在一起,在方法语法中将这些查询组合起来。当你使用查询语法编写查询,编译器会将其转换为这种形式。由于变量不存储查询结果,所以可以随时修改它或将其用作新查询的基础。