附上WORD文档资料,从这里下载
LINQ编译模块
在前面已经介绍了C#的一些语言扩展。实际上正是这些语言扩展使得LINQ成为可能。如果你足够仔细你会发现,这些扩展需要新的C#编译器来解释这些新的特性。但是并没有改变IL指令和.NET的运行时。也就是说这些其实只是改变了我们编写代码的习惯而已。
在这一章我们将会进一步看LINQ是如何把前面说到的这些新的语言特征完美的运用在一起的,从而形成了一个LINQ的编译模块。然后将深入的了解一些其他概念。
上一章的示例经过最终的简化可以写成这个样子:
var processes = Process.GetProcesses() .Where(process => process.WorkingSet64 >= 20 * 1024 * 1024) .OrderByDescending(process => process.WorkingSet64) .Select(process => new { ID = process.Id, Name = process.ProcessName}); |
这就是一个LINQ查询。如果你仔细看的话你就会发现很多我们前面说道的语言扩展。如下面所示:
由此可见,我们前面说道的那些语言扩展对LINQ来说是多么的重要。
在这一章中,我们的重点将放在进一步了解LINQ是如何将这些关键的语言扩展有机的结合在一起形成查询模块的。这章的内容将包括:
· 序列(Sequences)及其在LINQ查询中的运用。
· 查询表达式,如:from …where…select等语法。
· 查询操作符。
· 延迟执行的含义及意义。
· 表达式树及它对延迟执行的支持。以及它的使用。
序列(Sequences)
先来看我们前面的例子,注意新添加的注释:
// Get the list var processes = Process.GetProcesses() // Filter the list .Where(process => process.WorkingSet64 >= 20 * 1024 * 1024) // Sort the list .OrderByDescending(process => process.WorkingSet64) // Select the fields we need .Select(process => new { ID = process.Id, Name = process.ProcessName}); |
为了深入理解上面的代码,我们首先来看看IEnumerable<T>接口。
IEnumerable<T>
IEnumberable<T>是.NET支持迭代的一个重要手段。迭代器使我们能很容易的遍历集合。它类似于Python的Generator和DB中经常用到的游标。对于迭代器,你应该知道C#的foreach循环。它就是通过迭代器来实现对集合的遍历的。在.NET中,很多集合都实现了IEnumberable<T>接口,如List<T>、Dictionary<T>和ArrayList等。在这些集合中,通过实现IEnumerable<T>的GetEnumerator()方法来取得一个对象,用来遍历集合中的元素。在深入了解迭代器以前,我们可以先假设GetEnumerator()返回的就是集合本身的一个镜像。当然这个镜像是按照IEnumberable<T>规定的格式来组织数据的。而.NET则对这个镜像进行了优化,它不要求一次性的全部取出数据,而是只取出以部分。为了控制这种一次只取部分数据,.NET定义了一系列方法,这些方法被封装在IEnumerator<T>接口中。因此,每个实现了IEnumerable<T>接口的集合都可以使用迭代。下面是IEnumerable<T>和IEnumerator<T>中要求实现的方法和属性。
IEnumerable
IEnumerator<T> GetEnumerator()
IEnumerator
bool MoveNext() IEnumerator Current void Reset void Dispose
IEnumerable<T>
IEnumerator<T> GetEnumerator() IEnumerator GetEnumerator()
IEnumerator<T>
bool MoveNext() IEnumerator<T>.Current IEnumerator Current
void Dispose()
void Reset() |
需要注意的是上面的有下划线的方法。由于IEnumerable<T>继承自IEnumerable,所以必须同时实现IEnumerable的方法。对IEnumerator也是同样的道理。
现在来看一个简单的示例。这个示例首先定义了一个集合,如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text;
namespace LINQBuildBlock { public class CityCollection:IEnumerable<string> { string[] m_Cities = { "New York", "Paris", "London" };
class MyEnumerator : IEnumerator<string> { CityCollection m_collection; int m_Current;
public MyEnumerator(CityCollection collection) { m_collection = collection; m_Current = -1; }
public bool MoveNext() { m_Current++; if (m_Current < m_collection.m_Cities.Length) { return true; } else { return false; } }
string IEnumerator<string>.Current { get { if (m_Current == -1) throw new InvalidOperationException(); return m_collection.m_Cities[m_Current]; } }
object System.Collections.IEnumerator.Current { get { if (m_Current == -1) throw new InvalidOperationException(); return m_collection.m_Cities[m_Current]; } }
public void Dispose() { throw new NotImplementedException(); }
public void Reset() { m_Current = -1; } }
IEnumerator<string> IEnumerable<string>.GetEnumerator() { return new MyEnumerator(this); }
IEnumerator IEnumerable.GetEnumerator() { return new MyEnumerator(this); } } }
|
由于我们使用了泛型的IEnumerator<T>和IEnumerable<T>,我们必须同时实现非泛型的相应方法。我们可以像下面一样来遍历:
foreach (var city in cities) { Console.WriteLine("This is city " + city); } |
这不仅简化了调用时的复杂性,还使得代码具有更好的移植性。即,不管集合是什么类型,只要实现了IEnumerable接口,我们就可以用上面的方法来调用。从上面的代码也可以出看出,IEnumerable<T>对底层集合的操作进行了封装。我们可以按照自己需要的方式来遍历元素,并且不用一次加载所有元素。对于大数据量的处理这非常有用。例如,如果要操作一个数据库并将数据分页显示在页面上。我们就可以自己顶一个类,将数据源指定在一个数据库中。同时实现MoveNext和Current等方法。
需要注意的是,对IEnumerable<T>和IEnumerator<T>进行遍历时操作的对象实际上是一个IEnumerator的实例。这很重要。遍历的时候我们并不知道底层数据是怎么来的,因为IEnumerator为我们准备好了需要的数据。因此,IEnumerator操作的底层数据并不一定非得是集合。只要能按照需要返回一个正确的结果就可以。现在看下面的代码:
// Keep in mind this method works as a collection 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; } |
在进一步说明上面的代码以前先解释一下yield return。这是C# 2.0中引入的新的概念。必须在实现了IEnumerable接口或者返回IEnumerable的地方使用yield return。对于开发者,它的作用是能记录每次返回时的一个状态,并在下一次执行的时候直接从上一次执行的下一句开始执行。即,不用每次都从起始位置执行。另外,在这里,我们必须使用yield return返回。如果使用return返回将会打乱遍历的顺序,因此会有编译错误。
对于上面的代码,我们可以把OneTwoTree()函数当成一个集合来遍历,如:
foreach (var number in OneTwoThree()) { Console.WriteLine("Return Value: " + number.ToString()); } |
实际上,编译器在遇到yield return时将会做一些处理。下面是IL代码反编译的结果:
|
在这里需要注意到,return时返回的是一个类。即,编译器自动为我们创建了一个类。再看返回的这个类的定义:
|
从上面我们可以看出下面几点:
1. 编译器自动生成的类实现了IEnumerable<T>、IEnumerable、IEnumerator<T>和IEnumerator接口。这就和我们前面的一个示例一样了。
2. 对于yield return,编译器会有一个switch开关,来为每一个yield return返回一个值。这就解释了前面说的,每到一个yield return,程序就会返回一值,同时保留一个状态。下一次执行的时候程序将会从上一次记录的地方继续执行。
3. 上面的代码还可以看出,IEnumerable是线程安全的,它使用了一个开关来保证每次只有一个线程在操作数据。
关于IEnumerable,现在能理解的也就这些。我在网上查了很多资料,写得都很专业化,看完了还是啥都不知道。希望我的理解对大家有所帮助。
延迟执行
LINQ的查询严重依赖于滞后的执行。在LINQ的词汇中,我们叫做延迟执行。对于LINQ来说,这是一个非常重要的概念。首先看一个例子来弄清楚什么是延迟执行。
using System; using System.Collections.Generic; using System.Linq; using System.Text;
namespace LINQBuildBlock { class Program { static void Main(string[] args) { int[] numbers = { 1, 2, 3 };
var result = from n in numbers select Square(n);
foreach (var n in result) { Console.WriteLine(n); } }
static double Square(double n) { Console.WriteLine("Computing Square(" + n + ")...");
return Math.Pow(n, 2); } } }
|
上面的执行结果如下:
如果不是延迟执行,在定义result的时候就应该输出了【Computing Square(1)…】之类的。即,执行结果应该是
Computing Square(1)… Computing Square(2)… Computing Square(3)… 1 4 9 |
而事实不是这样的。这说明查询语句是在需要遍历结果集的时候执行的。这很重要。延迟查询只是在需要的时候才载入数据,这可以尽可能的提高效率。试想,如果有成千上万条数据,如果一次性的加载入内存,将会消耗掉很多资源。但是,如果只是在需要的时候才载入某些资源,将会极大的节省内存控件。这就是延迟查询的处理方式。因此,延迟查询允许我们定义一个资源,而在以后再使用他们。
由于延迟查询是在我们需要的时候才载入数据,也就是说如果在定义了一个延迟查询后,数据源被更新了,那么我们在遍历的时候将得到最新的数据。即,延迟查询总是查询到最新的数据。看下面的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text;
namespace LINQBuildBlock { class Program { static void Main(string[] args) { int[] numbers = { 1, 2, 3 };
var result = from n in numbers select Square(n);
// Iterate the query. foreach (var n in result) { Console.WriteLine(n); }
// Update the data source for (int i = 0; i < numbers.Length; i++ ) { numbers[i] += 10; }
Console.WriteLine("\n==Collection Updated==\n");
// Requery the collection foreach (var n in result) { Console.WriteLine(n); } }
static double Square(double n) { Console.WriteLine("Computing Square(" + n + ")...");
return Math.Pow(n, 2); } } }
|
执行结果如下。可以看见,当我们更新了数据源之后,再次执行查询取得的是最新的结果。
如果希望在定义好一个查询后立刻取得它的结果,可以使用ToList方法。该方法是IEnumberable的一个扩展方法。在这个示例中,它将返回一个List<double>的集合。与前面的延迟查询相反,它执行后将把结果加载到内存中。当我们再次执行查询的时候,得到的结果可能就与List<double>中的结果不一样了。
查询操作符
查询操作符就不用多说了。我们先介绍一下它是什么样子的,然后列出.NET支持的查询操作符。需要说明的是,这些查询操作符都是IEnumerable<T>的扩展方法。
下面是一个查询操作符的例子:
var processes = Process.GetProcesses() .Where(proc => proc.WorkingSet64 >= 20 * 1024 * 1024) .OrderByDescending(proc => proc.WorkingSet64) .Select(proc => new { ID = proc.Id, Name = proc.ProcessName, Memory = proc.WorkingSet64 }); |
因此可以看出,查询操作就像是一个工厂,而每一个操作符就是一个工厂里面的一台机器。数据就是原材料。原材料经过每一台机器的处理,最后就是我们期望的结果。如图:
但是,不要被上面的图误导。要知道查询有延迟执行的特点。也就是说,只有当我们需要遍历数据的时候才会启动上面的处理流程。
下面是.NET中的一些标准操作符:
Family | Query operators |
Filtering | OfType, Where |
Projection | Select, SelectMany |
Partitioning | Skip, SkipWhile, Take, TakeWhile |
Join | GroupJoin, Join |
Concatenation | Concat |
Ordering | OrderBy, OrderByDescending, Reverse, ThenBy, ThenByDescending |
Grouping | GroupBy, ToLookup |
Set | Distinct, Except, Intersect, Union |
Conversion | AsEnumerable, AsQueryable, Cast, ToList, ToDictionary. |
Equality | SequenceEqual |
Element | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault. |
Generation | DefaultIfEmpty, Empty, Range, Repeat. |
Quantifier | All, Any, Contains |
Aggregation | Aggregate, Average, Count, LongCount, Max, Min, Sum. |
查询表达式
到现在为止,我们的查询都是基于方法调用的样式的。但是正如你所见的大多数的LINQ语句一样是一种查询表达式样式的。如下:
var processes = from proc in Process.GetProcesses() where proc.WorkingSet64 >= 20 * 1024 * 1024 orderby proc.WorkingSet64 descending select new { ID = proc.Id, Name = proc.ProcessName, Memory = proc.WorkingSet64 }; |
可以看出,查询表达式提供了一种类似SQL语句的更简洁的查询方式。实际上,查询表达式是一种C#提供的集成在语言中的语法。它允许我们对一个或多个数据源使用操作符。当我们使用查询表达式的时候编译器会翻译成为标准的基于方法调用的查询操作符来执行。
查询表达式的语法一般如下:
一般的,查询表达式以From开始。From指定了数据源。一个查询表达式中可以有多个数据源,即可以有多个from关键字。Join关键字指定了多个数据源的连接方式(内敛、外联或者笛卡尔集等)。Let关键字指定了连接条件。其他的关键字都很熟悉了。在后面还会有介绍。
下面看看查询操作符和查询表达式在IL中的结果:
查询操作符:
Process.GetProcesses() .Where<Process>( delegate (Process proc) { return (proc.WorkingSet64 >= 0x1400000L); }) .OrderByDescending<Process, long>( delegate (Process proc) { return proc.WorkingSet64; }) .Select(delegate (Process proc){ return new { ID = proc.Id, Name = proc.ProcessName, Memory = proc.WorkingSet64 };}); |
查询表达式:
Process.GetProcesses() .Where<Process>(delegate (Process proc) { return (proc.WorkingSet64 >= 0x1400000L); }) .OrderByDescending<Process, long>(delegate (Process proc) { return proc.WorkingSet64; }) .Select(delegate (Process proc) { return new { ID = proc.Id, Name = proc.ProcessName, Memory = proc.WorkingSet64 };});
|
可以看见,他们在IL代码中的结果都是一样的。可能你也注意到了,前面提到的拉姆计算符在这里也转化成了匿名函数。因此拉姆表达式只是C#语法的变化,而不是IL层面变化。
下面的表标准的查询操作符和查询表达式的对应关系:
Query operator | C# syntax |
All | N/A |
Any | N/A |
Average | N/A |
Cast | 使用显示的类型转化变量,如: from int i in array |
Count | N/A |
Distinct | N/A |
GroupBy | group ... by... 或 group ... by... into... |
GroupJoin | Join ... in ... on ... equals ... into ... |
LongCount | N/A |
Max | N/A |
Min | N/A |
OrderBy | orderby |
OrderByDescending | orderby ... descending |
Select | select |
SelectMany | 使用多个from关键字 |
Skip | N/A |
SkipWhile | N/A |
Sum | N/A |
Take | N/A |
TakeWhile | N/A |
ThenBy | orderby ..., ... |
ThenByDescending | orderby ..., ... descending |
Where | Where |
由此可见,并不是所有的查询操作符都能有对应的查询表达式。这就为我们在使用查询表达式的时候带来了限制。但是,查询表达式进一步的优化了拉姆计算符,使我们不必每次都定义参数,也使得查询语句更具可读性。所以,这种取舍又开发人员自己决定。
表达式树
在前面我们已经提到过大幕表达式树。但是我们并没有说明它是用来做什么的。现在先看看什么是表达式树。下面是将一个拉姆表达式树赋给一个委托对象:
Func<int, bool> Odd = i => (i & 1) ==1; |
可以看见,这没有什么特别之处。但是,我们在使用Odd的时候可以将它当作一个数据而不是代码。如下面所示:
for (int i = 0; i < 10; i++) { if (Odd(i)) { Console.WriteLine(i.ToString() + "is odd."); } else { Console.WriteLine(i.ToString() + "is even."); } } |
再来看看表达式树。表达式树是指将一个拉姆表达式赋给Expression<TDelegate>。如下所示:
Expression<Func<int, bool>> IsOdd = i => (i & 1) == 1; |
编译器在发现拉姆表达式被赋给了一个Expression<TDelegate>的时候将采取和赋给委托完全不一样的处理方式。如下面是赋给委托的时候的IL代码:
而下面是赋给Expression<TDelegate>的IL代码:
为了便于了解它的结构,我们对IL代码进行了优化,使其看起来更直观。可以看出编译器将拉姆表达式转化成了一系列的方法调用。下面是更直观的树形表示:
至于表达式树的作用,我们在后面将会提到。它可以在运行时赋给一个对象,让他们去执行,或者转化为其他的执行方式。即,它使得动态查询成为可能。如在SQL中,使用表达式树来产生SQL语句,并执行查询。
表达式树还有另一个作用,它是另一种延迟查询的基础。前面我们说道延迟查询是基于IEnumerable<T>的。而表达式的延迟查询是基于IQuery的。由于基于IQuery的延迟查询具有智能分析的能力(得益于表达式树),所以它有更广泛的用途。在这里我们不详细说明。后面的章节会有专门的介绍。
下面是本章的基本内容:
· 序列(Sequences)及其在LINQ查询中的运用。
· 查询表达式,如:from …where…select等语法。
· 查询操作符。
· 延迟执行的含义及意义。
· 表达式树及它对延迟执行的支持。以及它的使用。