LINQ 构建块
下面我们将回顾一下我们上一章学习的新语言特性的摘要。并且我们将展示形成LINQ中关键元素的新特性。而且我们将着重讲述语言扩展和关键概念。这包括序列,标准查询操作符,查询表达式和表达式树。在本章的结尾,我们将看一下LINQ扩展的.NET Framework中的程序集和命名空间。
3.1 LINQ 如何扩展了.NET
本节我们将回顾在第二章中介绍的特性,并把它们综合到一起,以便从中得知它们是如何契合到一起的。
3.1.1 语言扩展回顾
作为回顾,让我们总结一下第二章讲述的语言特性:
n 隐式类型局部变量
n 对象初始化器
n Lambda 表达式
n 扩展方法
n 匿名类型
这些就是我们所讲的语言扩展,是为支持LINQ加入到C#中的语法构造新特性。所有这些扩展需要新版本的C#编译器的支持,但不需要新的.NET运行时支持。
为了介绍LINQ的概念,我们将会分析一些示例代码,下面是上一章介绍的代码:对正在运行的进程进行过滤和排序:
static void DisplayProcesses()
{
var processes =
Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.OrderByDescending(process => process.WorkingSet64)
.Select(process => new { process.Id, Name=process.ProcessName });
ObjectDumper.Write(processes);
}
粗体部分是LINQ查询。仔细观看,你会发现我们介绍的很多语言新特性,如图3.1所示:
在本图中,你会发现这些语言新特性是如何组合在一起的。
图3.1 所有的语言扩展都在本图中
3.1.2 LINQ 基础的关键元素
除了我们刚才列出的语言扩展外,LINQ需要其他的一些特性和概念支持,下面是LINQ查询相关的概念:
n 我们将解释什么是序列,它们如何应用在LINQ查询中。
n 你将会遇到查询表达式,形如:from…where…select 的语法
n 我们将研究查询操作符,在LINQ查询中表示可执行的基本的操作。
n 我们将解释什么是“延迟的查询执行”,为什么这很重要。
n 为了启用延迟查询,LINQ使用了表达式树,我们将会学到什么是表达式树,LINQ是如何使用它们的。
为了读写LINQ代码,你需要理解这些特性。
3.2 序列介绍
本章我们展示的第一个LINQ概念是序列,为了理解什么是序列,让我们分析列表3.1的代码。
列表 3.1 使用扩展方法查询进程列表
var processes = Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.OrderByDescending(process => process.WorkingSet64)
.Select(process => new { process.Id, Name=process.ProcessName });
以处理的发生为顺序,让我们一步一步的分析此代码。
首先介绍一下IEnumerable<T>,一个LINQ中无处不在的关键接口。我们将会对迭代器进行一个简要的回顾,然后着重讲解迭代器是如何允许延迟的查询执行的。
3.2.1 IEnumerable<T>
列表3.1中首先调用了Process.GetProcesses,该方法在System.Diagnostics.Process类中,它返回了一个进程对象的数组。该方法没有什么特别的地方,值得注意的是它实现了IEnumerable<T>接口。该接口在.NET2.0中出现,是LINQ的关键。在本例中一个Process对象的数组实现了IEnumerable<Process>
IEnumerable<T>很重要,因为LINQ中Where , OrderBy- Descending , Select和其他标准查询操作符都需要这种类型的对象作为参数。
列表3.2显示了Where方法的定义,如:
列表 3.2 示例查询中使用的Where 方法
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, Boolean> predicate)
{
foreach (TSource element in source)
{
if (predicate(element))
yield return element;
}
}
这个Where方法来自何方?它是IEnumerable<T>接口的方法吗?答案是否定的。想到扩展方法了吧。这是一个接受IEnumerable<T>为第一个参数的扩展方法。
这里我们看到的( OrderByDescending, Select等)扩展方法是System.Linq.Enumerable类提供的。类名基于这样一个事实,它的所有方法都是工作在IEnumerable<T>对象之上。
注意 在LINQ中,术语“序列”指任何实现IEnumerable<T>接口的任何对象。
让我们再看一下Where方法。它使用了yield return语句,以及IEnumerable<TSource>类型的返回值。
继续之前,让我们回顾一下迭代器的知识。
3.2.2 迭代器回顾
Iterator是一个允许遍历集合元素的对象。你一定知道什么是iterator,而且经常使用它。当时使用foreach循环的时候,iterator将被使用。每个.NET集合都有一个名为GetEnumerator的方法,它返回一个允许遍历其内容的对象。这是foreach背后使用的遍历方法。
如果你对设计模式感兴趣,你可以学习经典的迭代器设计模式。
iterator是很简单的,从结果来看,传统的方法是返回一个集合,因为它产生一个序列值。例如我们可以创建如下方法返回一个整数列表。
int[] OneTwoThree()
{
return new [] {1, 2, 3};
}
然而C#2.0或者3.0的行为是特殊的。它并不是创建一个包含所有值的集合,然后依次返回它。而是一次返回一个值。这需要的内存更少,允许调用者很快的处理前几个值,而不必预先准备这个集合。
让我看看iterator是如何工作的。创建一个iterator是很简单的:使用一个返回值列表,这些值使用yield return 返回。
列表3.3显示了一个名为OneTwoThree的迭代器方法:
列表 3.3 示例迭代起
using System;
using System.Collections.Generic;
static class Iterator
{
static IEnumerable<int> OneTwoThree()
{
Console.WriteLine("Returning 1");
yield return 1;
Console.WriteLine("Returning 2");
yield return 2;
Console.WriteLine("Returning 3");
yield return 3;
}
static void Main()
{
foreach (var number in OneTwoThree())
{
Console.WriteLine(number);
}
}
下面是示例代码的执行结果:
Returning 1
1
Returning 2
2
Returning 3
3
如你所见,OneTwoThree方法指导我们的示例代码结束时才真正退出。每次我们遇到yield return语句,控制就返回到调用方法。在我们的示例中,foreach循环执行它的工作,然后控制又返回到迭代器方法中,从它上次离开的地方开始执行并提供下一个返回项。
这看上去好像是两个方法在同时运行。这就是.NET迭代器被展示为一种轻量级协同路由的原因。传统的方法在方法体的开始处执行,而这种方法被命名为子路由。作为比较,协同路由是一个可以在上次停止的地方重新执行的方法。所有的C#方法都是子路由方法,除了包含yield return构造的方法。
令人感到奇怪的是,我们实现了一个返回IEnumerable<int>的方法。而实际上我们并没有返回这种类型的对象。当使用yield return时,编译器为我们做了所有的工作。一个执行IEnumerable<int>接口的类被自动创建。
我们不会在这个主题上继续探讨,因为并不是理解LINQ必须的。
这个简单的示例显示了迭代器基于懒惰计算。我们要强调这个特点,因为它是LINQ所必需的。
3.2.3 延迟的查询执行
LINQ查询完全依赖懒惰计算。在LINQ词典中,这指的是延迟的查询执行。这是LINQ中最重要的概念。没有这个特点,LINQ会变得缓慢。
让我们举例阐明LINQ查询的执行行为。
阐明延迟的查询执行
在列表3.4中,我们对一个整数数组查询。并执行了一个操作。
列表 3.4 延迟查询执行阐述
using System;
using System.Linq;
static class DeferredQueryExecution
{
static double Square(double n)
{
Console.WriteLine("Computing Square("+n+")...");
return Math.Pow(n, 2);
}
public static void Main()
{
int[] numbers = {1, 2, 3};
var query =
from n in numbers select Square(n);
foreach (var n in query)
Console.WriteLine(n);
}
}
程序的执行结果显示,查询没有被一次执行,而是在每次迭代的时候计算:
Computing Square(1)...
1
Computing Square(2)...
4
Computing Square(3)...
9
如在3.4节中所讲,如下查询将会在编译时转换为方法调用:
var query = from n in numbers select Square(n);
编译后,这个查询变为:
IEnumerable<double> query =
Enumerable.Select<int, double>(numbers, n => Square(n));
因为Enumerable.Select方法返回一个迭代器,这就解释了,为什么我们获得了延迟执行。
我们的查询变量并不表示查询结果,而仅仅是一个执行查询的可能。直到它被赋值给其它变量的时候,查询才被执行。
延迟查询执行的一个优点是节约资源。只有当你在查询结果上进行迭代的时候,查询才被执行。假设我们的查询返回了上千个元素。如果我们决定看一下第一个元素,然后决定不需要继续处理,那么这些结果将不会载入内存。这是因为这种结果是作为序列提供的。如果这种结果包含在一个传统的数组或者列表中。它们将会被全部载入到内存中,而不管我们需要不需要它。
重用查询来获取不同的结果
有一点需要完全理解,如果在同一个查询上迭代两次,那么可能会获得不同的结果。列表3.5的代码显示了这一点:
列表 3.5 同一个查询在两次执行期间产生不同的结果
using System;
using System.Linq;
static class QueryReuse
{
static double Square(double n)
{
Console.WriteLine("Computing Square("+n+")...");
return Math.Pow(n, 2);
}
public static void Main()
{
int[] numbers = {1, 2, 3};
var query = from n in numbers select Square(n);
foreach (var n in query)
Console.WriteLine(n);
for (int i = 0; i < numbers.Length; i++)
numbers[i] = numbers[i]+10;
Console.WriteLine("- Collection updated -");
foreach (var n in query)
Console.WriteLine(n);
}
}
在使用了查询对象后,我们改变了集合,然后再次使用查询对象。正如我们期望,两次查询结果是不同的:
Computing Square(1)...
1
Computing Square(2)...
4
Computing Square(3)...
9
- Collection updated - Computing Square(11)...
121
Computing Square(12)...
144
Computing Square(13)...
169
第二次迭代重新执行了查询,产生了新的结果。
强制立即执行查询
如上所示,延迟执行是默认的行为。查询只有在请求数据的时候才被执行。如果你想即时执行,就需要显式请求。
也就是说,我们想要查询在我们处理结果之前完全执行。下面是没有延迟查询的结果:
Computing Square(1)...
Computing Square(2)...
Computing Square(3)...
1
4
9
做到这点,我们只需要调用ToList方法- System.Linq.Enumerable 类的另一个扩展方法:
foreach (var n in query.ToList())
Console.WriteLine(n);
虽然修改很简单,结果却完全不同。
ToList方法对查询进行迭代并创建List<double>的实例,并以查询结果进行初始化。现在foreach是对一个已填充的集合进行迭代了。
让我们回到DisplayProcesses示例,继续分析这个查询。
列表3.1中使用的Where, OrderByDescending,和Select方法是迭代方法。这表示在对结果进行迭代之前,这些方法并不会被调用。
3.3 查询操作符介绍
我们在示例中多次使用了System.Linq.Enumerabl类提供的扩展方法。现在让我们更详尽的了解一下它。这些方法被称作查询操作符,它们是LINQ的基础。
在介绍标准的查询操作符之前,首先应该了解一下查询操作符的定义。
3.3.1 什么是查询操作符?
查询操作符不是一个语言扩展,但却是对.NET Framework的类库的扩展。查询操作符是在LINQ查询上下文中执行操作的扩展方法的集合。
下面,看看我们对Where方法的使用:
var processes = Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.OrderByDescending(process => process.WorkingSet64)
.Select(process => new { process.Id, Name=process.ProcessName });
列表3.2显示Where方法的实现代码:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source, Func<TSource, Boolean> predicate)
{
foreach (TSource element in source)
{
if (predicate(element))
yield return element;
}
}
Where方法接受一个IEnumerable<T>作为参数。并且返回一个IEnumerable<T>对象,更确切的说是IEnumerable<Process>对象。
下面是Where方法工作内容:
1. 在进程对象列表上循环
2. 对进程列表过滤
3. 返回过滤后的进程列表
OrderByDescending 和 Select方法与Where方法工作方式类似,由此我们可以看出,这些方法是对原始列表对象进行提炼。类似于管道模式,你认为呢?
将示例代码转换为静态方法调用后如3.6所示:
列表 3.6 表达为静态方法调用的查询
var processes = Enumerable.Select(
Enumerable.OrderByDescending(
Enumerable.Where( Process.GetProcesses(),
process => process.WorkingSet64 > 20*1024*1024),
process => process.WorkingSet64),
process => new { process.Id, Name=process.ProcessName });
可以看到,扩展方法提高了代码的可读性。
我们强调的这些扩展方法如Where, OrderByDescending和Select的特点如下:
n 它们工作在序列上
n 它们允许管道数据处理
n 它们依赖延迟执行
所有这些特性对于编写查询非常有用。所以我们把它叫做查询操作符。
这是一个有趣的类比,如果我们把查询比作工厂,那么查询操作符就是机器或者引擎,序列就是机器的原料。
如列表3.6所示,我们可以说,查询就是一系列查询操作符的组合。是LINQ的关键。
3.3.2 标准查询操作符
查询操作符可以被合并用来对集合执行复杂操作。一些操作符被预先定义进行普遍的操作,这些操作符被称为标准查询操作符。
表3.1根据操作的类型对标准操作符进行了分类
表3.1 标准查询操作符家族
家 Family | 操作符 Query operators |
过滤 | OfType, Where |
投影 | Select, SelectMany |
分割 | Skip, SkipWhile, Take, TakeWhile |
连接 | GroupJoin, Join |
合并 | Concat |
排序 | OrderBy, OrderByDescending, Reverse, ThenBy, ThenByDescending |
分组 | GroupBy, ToLookup |
集合 | Distinct, Except, Intersect, Union |
转换 | AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList |
相等 | SequenceEqual |
元素 | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault |
产生 | DefaultIfEmpty, Empty, Range, Repeat |
限量 | All, Any, Contains |
聚合 | Aggregate, Average, Count, LongCount, Max, Min, Sum |
由于查询操作主要是工作在IEnumerable<T>对象上的扩展方法,我们可以很容易的创建我们自己的查询操作符。这一点将会在12章中讲述。
3.4 查询表达式介绍
我们在示例中一直使用基于方法的调用。这是一种表达查询的方式。但是大多数情况下,我们都使用一种不同的语法:查询表达式。
我们将阐述什么是查询表达式以及查询表达式和查询操作符之间的关系。
3.4.1 什么是查询表达式?
查询运算符是允许表达查询的静态方法。但是相比使用如下语法:
var processes = Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.OrderByDescending(process => process.WorkingSet64)
.Select(process => new { process.Id, Name=process.ProcessName });
你可以使用另一种是LINQ查询更像SQL查询的方式进行查询:
var processes =
from process in Process.GetProcesses()
where process.WorkingSet64 > 20*1024*1024
orderby process.WorkingSet64 descending
select new { process.Id, Name=process.ProcessName };
这种查询语法叫做查询表达式。
两段代码完成的功能是一样的,而查询表达式更接近自然语言,书写也更方便。查询表达式允许我们使用查询操作符的强大功能的同时还具备了面向查询的语法。
查询表达式提供了与关系和层状查询语法(如SQL何XQuery)类似的语言集成的语法。一个查询表达式在一个或者多个信息源上进行多个标准查询操作或特定域的操作。在我们的示例中,查询表达式使用三个标准的查询操作符:Where, OrderByDescending, 和 Select。
当你使用查询表达式的时候,编译器自动转换查询到标准的查询操作符调用。
因为查询表达式编译为方法调用,所以它们不是必须的。我们可以直接使用查询操作符。使用查询表达式的最大优点是它使得代码有更高的可读性和保持代码简单。
3.4.2 使用查询表达式
让我们使用C#详述查询表达式的用法。
图3.3显示了查询表达式的完整语法。
图3.3 C# 查询表达式语法
让我们分析一下在C#3.0中这种语法是如何构造的。一个查询表达式以一个from字句开始,以select或者group字句结束。from子句后面可以零个或者多个from,let where, join或者orderby子句。
每个from字句引入序列中包含的元素。每个let子句引入一系列的由原始元素计算出的变量。每个where字句对结果进行过滤。
每个join字句对两个序列中的键值进行比较,产生匹配对。每个orderby字句根据排序标准对序列进行排序。最后的select或者group字句指定最后产生的结果的形式。
最终,一个into字句可以把一个查询产生的结果作为另一个子序列查询的产生器。
如果你了解SQL,这种语法你应该很熟悉。
3.4.3 标准查询操作符如何与查询表达式关联
当查询表达式编译为标准操作符的调用的时候,就会发生转换。
考虑如下查询表达式:
from process in Process.GetProcesses()
where process.WorkingSet64 > 20*1024*1024
orderby process.WorkingSet64 descending
select new { process.Id, Name=process.ProcessName };
使用查询操作符编写同样的查询如下:
Process.GetProcesses()
.Where(process => process.WorkingSet64 > 20*1024*1024)
.OrderByDescending(process => process.WorkingSet64)
.Select(process => new { process.Id, Name=process.ProcessName });
表3.2显示了主要的标准操作符是如何映射到新的C#查询表达式关键字的。
表 3.2 查询操作符和查询表达式之间的映射关系
查询操作符 | C# 语法 | VB.NET 语法 |
All | N/A | Aggregate … In … Into All(…) |
Any | N/A | Aggregate … In … Into Any() |
Average | N/A | Aggregate … In … Into Average() |
Cast | Use an explicitly typed range variable, for example: from int i in numbers | From … As … |
Count | N/A | Aggregate … In … Into Count() |
Distinct | N/A | Distinct |
GroupBy | group … by or group … by … into … | Group … By … Into … |
GroupJoin | join … in … on … equals … into… | Group Join … In … On … |
Join | join … in … on … equals … | From x In …, y In … Where x.a = b.a or Join … [As …] In … On … |
LongCount | N/A | Aggregate … In … Into LongCount() |
Max | N/A | Aggregate … In … Into Max() |
Min | N/A | Aggregate … In … Into Min() |
OrderBy | orderby | Order By |
OrderByDescending | orderby … descending | Order By … Descending |
Select | select | Select |
SelectMany | Multiple from clauses | Multiple From clauses |
Skip | N/A | Skip |
SkipWhile | N/A | Skip While |
Sum | N/A | Aggregate … In … Into Sum() |
Take | N/A | Take |
TakeWhile | N/A | Take While |
ThenBy | orderby …, … | Order By …, … |
ThenByDescendin | orderby …, … descending | Order By …, … Descending |
Where | where | Where |
如上所见, 并不是所有的操作符都有等价的关键字与之对应。在简单的查询中,你可以使用编程语言支持的关键字,但是对于高级查询,你还是需要直接调用操作符。
同样,使用查询表达式编写查询只是为了方便键入和提高可读性,最后在编译的时候,查询表达式还是转换为标准的查询操作符。你可以决定只使用查询操作符,而不是用查询表达式,如果你喜欢这么做的话。
3.4.4 缺点
C#编译器将查询表达式转换为下列操作符的调用:: Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDe- scending, ThenBy, ThenByDescending, GroupBy, 和Cast等.如果你需要使用其他操作符,你可以在查询表达式中直接调用。
如列表3.7所示,我们使用了Take和Distinct操作符。
列表 3.7 使用查询操作符的C#查询表达式
var authors =
from distinctAuthor in (
from book in SampleData.Books where book.Title.Contains("LINQ")
from author in book.Authors.Take(1)
select author)
.Distinct()
select new {distinctAuthor.FirstName, distinctAuthor.LastName};
我们显式使用Take和Distinct。其他操作符是隐式使用的。如Where,Select和SelectMany,它们与where,select和from关键字对应。
在列表3.7中,查询选择书名中包含“LINQ”的第一个作者的名字。
列表3.8显示了用操作符编写的同一个查询。
列表 3.8 仅使用查询操作符的C#查询
var authors = SampleData.Books
.Where(book => book.Title.Contains("LINQ"))
.SelectMany(book => book.Authors.Take(1))
.Distinct()
.Select(author => new {author.FirstName, author.LastName});
你来决定它们两个的可读性。在有些情况下,你更喜欢使用合并的查询操作符,因为查询表达式不会让事情变得简单,甚者,他会使代码变得更难理解。
在列表3.7中,你可以看见为了调用Distinct操作符,我们需要把调用者用括号括起来。这种在查询表达式中间的括号会使代码更难阅读。在列表3.8中,我们可以很容易的理解这种管道处理。查询操作符允许我们顺序的组织操作。
如果你熟悉SQL,你可能更喜欢使用查询表达式,因为它们有相似的语法。另一个让你选择查询表达式的原因是它提供了更简洁的语法。
如以下示例,首先我们使用查询操作符进行查询:
SampleData.Books
.Where(book => book.Title == "Funny Stories")
.OrderBy(book => book.Title)
.Select(book => new {book.Title, book.Price});
下面是使用查询表达式的同一个查询:
from book in SampleData.Books
where book.Title == "Funny Stories"
orderby book.Title
select new {book.Title, book.Price};
两个查询时等价的。但是你可能会注意到使用查询操作符的查询大量的使用了lambda表达式。Lambda表达式很有用,但是过多的lambda表达式会让代码难于阅读。同样,在这个查询中,book标识符被多次声明,而查询表达式中,你可以看到book标识符之声明了一次。
再次强调,这只是个人喜好的问题,所以我们不会推荐一种方式会好于另一种方式。
3.5 表达式树介绍
表达式树可能并不常用。但却是LINQ重要的组成部分。他们允许对LINQ进行更高级的扩展,如SQL LINQ。
在介绍表达式树之前,让我再回顾一下lambda表达式。然后再详述什么是表达式树以及表达式树启用延迟查询执行的另一种方法。
3.5.1 lambda 表达式的返回值
在第二章中,我们介绍了lambda表达式,主要讲了它作为一种表达匿名代理的新方式,可以被复制给代理类型,下面是更多的示例:
Func<int, bool> isOdd = i => (i & 1) == 1;
这儿我们使用了Func<T, TResult> 泛型代理,他定义在System命名空间中。在.NET3.5中,它是如下定义的:
delegate TResult Func<T, TResult>(T arg);
我们的IsOdd代理对象表示一个接受整数作为参数并返回bool类型的方法。这个代理变量可以像其他代理一样使用:
for (int i = 0; i < 10; i++)
{
if (isOdd(i))
Console.WriteLine(i + " is odd");
else
Console.WriteLine(i + " is even");
}
我们要强调的一点是,lambda表达式可以被用作数据而不是代码。这就是表达式树的由来。
3.5.2 什么是表达式树?
考虑使用定义在System.Linq.Expressions 命名空间中的Expression<TDelegate>类型的如下代码:
Expression<Func<int, bool>> isOdd = i => (i & 1) == 1;
这次,我们不能将isOdd作为一个代理使用了,这是因为它不是一个代理,而是一个表达式树。
当编译器知道这是一个Expression<TDelegate>类型,他会与对待Func<T, TResult>类型的方式有所不同。编译器不会编译lambda表达式为IL代码,而是产生构造表示表达式的树对象的代码。
注意到只有带有表达式体的lambda表达式才能成为表达式树。使用语句体的lambda表达式不能转换为表达式树。在接下来的示例中,第一个lambda表达式可以被用来声明一个表达式树,因为他有一个表达式体。而第二个则不可以,因为他包含语句体:
Expression<Func<Object, Object>> identity = o => o;
Expression<Func<Object, Object>> identity = o => { return o; };
当编译器看到lambda表达式被赋值到一个Expression<>类型的变量,他会把lambda表达式编译为一系列工厂方法调用。这些调用将会在运行时构造表达式树。下面是编译器在背后对我们的表达式产生的代码。
ParameterExpression i = Expression.Parameter(typeof(int), "i");
Expression<Func<int, bool>> isOdd =
Expression.Lambda<Func<int, bool>>(
Expression.Equal(
Expression.And(i,Expression.Constant(1, typeof(int))),
Expression.Constant(1, typeof(int))),
new ParameterExpression[] { i });
我们可以自己编写这样的代码,虽然这样做没有多大意义,但是在许多高级应用中却很有用。我们将会在第五章中介绍使用表达式树创建动态查询。
我们应该感谢编译器为我们产生了这些代码,你可以看到为什么我们称他为表达式树。图3.5是该树的图形化表示。
3.5表达式树的图形视图
到现在为止,我们学习了lambda表达式可以做为代码也可以作为数据。作为代理,lambda表达式可以产生IL代码,作为表达式树,产生可以表达该lambda的数据结构。
证明表达式描述一个lambda表达式的最好方法是表明表达式树可以被编译为代理:
Func<int, bool> isOddDelegate = i => (i & 1) == 1;
Expression<Func<int, bool>> isOddExpression = i => (i & 1) == 1;
Func<int, bool> isOddCompiledExpression =isOddExpression.Compile();
在这段代码中,isOddDelegate and isOddCompiledExpression是等价的,他们的IL代码是相同的。
现在你最想问的问题可能是:为什么我们需要表达式树?好的,一个表达式是一种抽象语法树(AST)。在计算机科学中,AST表示源代码解析后的数据结构。AST经常用于电脑程序在优化时使用的内部结构,然后被执行。在这里,表达式树是C#编译器在lmabda表达式上的解析结果。这么做的目标是有些代码能够分析表达式树并执行各种操作。
表达式树可以在运行时作为一个工具,使用这个工具可以将它们转换为其他的操作,如SQL等。在第4,5章你可以看到,SQL LINQ使用包含在表达式树中的信息产生SQL并执行对应的数据库的查询。
现在我们可以指出,表达式是另一种完成延迟查询执行的方法。
3.5.3 IQueryable, 又见延迟查询执行
你已经看到,IEnumerable<T> 和iterator可以让我们做到延迟查询执行,表达式树另一种查询处理的基础。
这是SQL LINQ中我们要用到的。当我们编写如下代码时,SQL直到foreach循环时才执行:
string path = System.IO.Path.GetFullPath(@"../../../../Data/northwnd.mdf");
DataContext db = new DataContext(path);
var contacts =
from contact in db.GetTable<Contact>()
where contact.City == "Paris"
select contact;
foreach (var contact in contacts)
Console.WriteLine("Bonjour "+contact.Name);
这种行为与使用IEnumerable<T>的情况类型,但是这次contacts变量的类型不是IEnumerable<Contact>,而是IQueryable<Contact>。IQueryable<T>与序列不同。一个IQueryable<T>的实例接受一个表达式树来检查它需要执行什么操作。
在这种情况下,一旦我们开始访问contacts变量,它包含的表达式树将被分析,产生SQL并执行,返回数据库的查询结果。
我们并不打算详述他们的工作原理,但是IQueryable比基于IEnumerable的序列更强大。因为我们可以对表达式树进行更智能的分析。通过检查该表达式产生的查询,有些工具可以对此进行强大的优化。IQueryable和表达式树在IEnumerable和管道模式不灵活的情况非常有用。
使用表达式树的延迟的查询执行允许SQL LINQ优化复杂的查询,从而产生更有效的查询。如果SQL LINQ使用类似IEnumerable<T>支持的管道模式。这种优势将不复存在。
在以后我们可以看到,表达式树和IQueryable可以用于扩展LINQ,而不只限于SQL LINQ。在12章中,我们将讲述如何利用LINQ的可扩展性。
3.6 LINQ DLLs 和命名空间
LINQ用到的类和接口来自一些列.NET3.5提供的程序集。你需要知道我们需要引用什么样的程序集合导入什么命名空间。
你使用的主要的程序集是System.Core.dll,为了编写LINQ对象查询,你需要导入该程序集包含的System.Linq命名空间。然后才可以使用System.Linq.Enumerable类提供的标准查询操作符。Vs2008将会自动引入该程序集和命名空间。
如果你需要跟表达式树一起工作来创建IQueryable的一个实现,你需要导入System.Linq.Expressions命名空间,该命名空间也是System.Core.dll提供的。
为了使用SQL LINQ和XML LINQ,你需要引入程序集System.Data.Linq.dll 和 System.Xml.Linq.dll,LINQ针对DataSet的特性在System.Data.DataSetExtensions.dll中提供。
System.Xml.Linq.dll和System.Data.DataSetExtensions.dll程序集会在你使用vs2008创建项目的时候自动引用。System.Data.Linq.dll 不会自动引入,你需要手工引入
表3.3是LINQ程序集和命名空间概览。
表 3.3 .NET 3.5为LINQ提供的程序集
System.Core.dll | |
System | Action and Func delegate types |
System.Linq | Enumerable class (extension methods for IEnumerable<T>) IQueryable and IQueryable<T> interfaces Queryable class (extension methods for IQueryable<T>) IQueryProvider interface QueryExpression class Companion interfaces and classes for query oper- ators: Grouping<TKey, TElement> ILookup<TKey, TElement> IOrderedEnumerable<TElement> IOrderedQueryable IOrderedQueryable<T> Lookup<TKey, TElement> |
System.Linq.Expressions | Expression<TDelegate> class and other classes that enable expression trees |
System.Data.DataSetExtensions.dll | |
System.Data | Classes for LINQ to DataSet, such as TypedTableBase<T>, DataRowComparer, DataTableExtensions, and DataRowExtensions |
System.Data.Linq.dll | Classes for LINQ to SQL, such as DataContext, Table<TEntity>, and EntitySet<TEntity> |
System.Data.Linq.Mapping | Classes and attributes for LINQ to SQL, such as ColumnAttribute, FunctionAttribute, and TableAttribute |
System.Data.Linq.SqlClient | The SqlMethods and SqlHelpers classes |
System.Xml.Linq.dll | |
System.Xml.Linq | Classes for LINQ to XML, such as XObject, XNode, XElement, XAttribute, XText, XDocument, and XStreamingElement |
System.Xml.Schema | Extensions class that provides extension methods to deal with XML schemas |
System.Xml.XPath | Extensions class that provides extension meth- ods to deal with XPath expressions and to create XPathNavigator objects from XNode instances |
3.7 摘要
在这一章中,我们解释了LINQ如何扩展了C#和.NET Framework。现在你应该对LINQ有了一个很好的了解。
我们了解了很多LINQ的重要组成部分。你学到很多新的技术和概念。
下面是我们对本章知识的总结:
n 序列,应用到LINQ的列举和迭代
n 延迟的查询执行
n 查询操作符,扩展方法允许在LINQ查询中使用
n 查询表达式,允许类似SQL from…where…select这样的语法
n 表达式树,表示将查询作为数据的展示,并运行高级扩展
下面我们终于可以写一些有用的LINQ代码了。在第二部分,我们使用对象LINQ进行内存对象的查询。第三部分,我们将使用SQL LINQ初始关系数据库的持久化。在第四部分,我们将详述使用XML LINQ操作XML文档。