1.简介
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的字符串:
var query = from cust in Customers
select cust.City;
- 若要选择多个多个属性,可以使用命名对象或匿名对象。如下,使用匿名类型封装各个Customer元素的两个属性:
var query = from cust in 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,所以可将方法链接在一起,在方法语法中将这些查询组合起来。当你使用查询语法编写查询,编译器会将其转换为这种形式。由于变量不存储查询结果,所以可以随时修改它或将其用作新查询的基础。