LINQ是.NET提供的一种快速处理数据的新方法。对于.NET框架来说他是一个新的模块,但是这个新的模块也是基于.NET的基本架构的。个人认为,学习LINQ不能简单的为了学习LINQ而学习LINQ。更应该从根本上了解LINQ的开发设计人员到底做了些什么工作使得LINQ成为可能。这本身也是一个进一步学习.NET基本知识的过程。因此,我才决定把自己的体会写下来,在学习LINQ的同时也回顾和加深一下对.NET的基本概念的认识。
我的整个学习过程是基于《Manning.LINQ.in.Action.Jan.2008.1933988169》的。由于资料文件太大,不能上传,有需要的可以和我联系,我直接传过来。
另外,笔记还有WORD文档,大家可以从这里下载。由于我也是在学习,因此有什么问题欢迎指正!
C#的语法扩展
为了支持LINQ,.NET进行了语法扩展。主要包括:
1. 定义隐式类型的本地变量。
2. 对象的初始化器(不是对象的构造器)。
3. 拉姆操作符(Lambda Expression)。
4. 扩展方法。
5. 隐式类型。
这一节将简单的介绍上面的语法扩展,并弄清楚它们是怎么样对LINQ进行支持的。为此,我们需要使用一个简单的示例来说明问题。示例只是简单的列出机器上所有的正在执行的进程。下面是源代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace LINQExample { class Program { static void Main(string[] args) { DisplayProcessesSimple(); } static void DisplayProcessesSimple() { List<ProcessData> processes = new List<ProcessData>(); foreach (Process process in Process.GetProcesses()) { ProcessData data = new ProcessData(); data.Id = process.Id; data.Name = process.ProcessName; data.Memory = process.WorkingSet64; processes.Add(data); } ObjectDumper.Write(processes); } } class ProcessData { public Int32 Id; public Int64 Memory; public String Name; } } |
接下来,我们将以这个源代码为基础,了解.NET语法扩展的详细情况。
隐式类型的本地变量
在.NET中,我们定义一个变量的时候通常是先写一个类型,然后是变量名,最后可能要给他赋值。编译器会检查你的赋值是否与定义的类型一致。从.NET 3.0开始,我们可以不用指定变量类型,而直接给一个变量赋值。编译器会根据你的赋值推导出你的变量的类型。这就是隐式类型的本地变量。语法如下:
var valName = Value; |
使用隐式类型的本地变量来优化我们的示例程序后的结果如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace LINQExample { class Program { static void Main(string[] args) { DisplayProcessesSimple(); } static void DisplayProcessesSimple() { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { var data = new ProcessData(); data.Id = process.Id; data.Name = process.ProcessName; data.Memory = process.WorkingSet64; processes.Add(data); } ObjectDumper.Write(processes); } } class ProcessData { public Int32 Id; public Int64 Memory; public String Name; } } |
由于ProcessData在很多地方都被定义成了本地的临时变量。因此我们可以使用隐式类型的本地变量来替代ProcessData。可能你现在还不能体会到隐式类型的本地变量给你带来的方便。但这只是第一步。在接下来的优化中,你会深深的体会到它的重要性。
对于隐式类型的本地变量,需要注意几点:
1. 隐式类型的本地变量必须在定义的时候就被初始化。理由很简单,编译器需要知道变量的类型。
2. 当为一个隐式类型的本地变量赋值后,它的类型其实已经被确定。在后面的可以改变变量的值,但是必须保持类型的一直。
3. 如果需要定义一个在程序中广泛使用的变量,建议还是不要使用隐式类型的本地变量。因为这容易导致产生类型错误。
对象初始化器
首先需要明白对象的构造和初始化是两个概念。在new一个对象的时候,我们通常会调用它的构造函数。但实际上,编译器会在调用构造函数之前插入对本地成员变量的初始化代码。在C#3.0以前,这些对类成员变量的初始化工作是封装起来的,由编译器完成。从C#3.0开始,我们可以在程序中做自定义的初始化操作。
语法
为一个类型使用初始化器的语法如下:
ClassName myClass = new ClassName(ConstructorParameters){IntitializeExpressions}; |
例如,为定义个Point, 我们可以像下面一样:
var pt = new Point { X = 1, Y = 2 };
ClassName myClass = new ClassName(strParam){IntProperty = 1};
可以在使用构造器的同时使用初始化器。
需要注意的是初始化器中初始化的属性或成员必须是公用的。
集合初始化器
对于那些实现了System.Collection.IEnumerable接口,并实现了Add方法的集合,我们还可以使用集合初始化器。例如,我们可以使用集合初始化器定义下面的一个集合:
var digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; |
List<T>实现了IEnumerable接口,并有一个Add方法,因此上面的写法是合法的。编译器的处理结果可能是:
List<int> digits = new List<int>(); digits.Add(0); digits.Add(1); digits.Add(2); … digits.Add(9); |
由此可见,使用集合的初始化器可以使代码更简洁、更易读。实际上,对于LINQ来说,对象初始化器和集合初始化器是使LINQ在形式上更像SQL语句的关键所在。例如,我们可以使用下面的代码来构造一个ProcessData的集合:
var processes = new List<ProcessData> { new ProcessData{Id = 123, Name = "devenv"}, new ProcessData{Id = 456, Name="FireFox"} }; |
使用初始化器优化示例
在使用初始化器后,我们的代码可以做下面的优化处理:
static void DisplayProcessesSimple() { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { processes.Add(new ProcessData { Id = process.Id, Name = process.ProcessName, Memory = process.WorkingSet64 }); } ObjectDumper.Write(processes); } |
在感觉到对象初始化器(包括集合初始化器)给我们带来的方便的时候需要提醒一下,对象初始化器并不能替代对象构造器。它们各自履行各自的职责,来方便的构造一个对象。有的地方我们使用初始化器更方便,而有的地方,我们可能不得不使用构造器。例如,如果我们需要经过一些业务处理来初始化一个属性或者成员,这时候使用初始化器明显就不合适了。另外,过多的使用初始化器可能导致数据处理的混乱。这点必须认识清楚。
拉姆计算符
拉姆计算符源自拉姆计算。它是很多功能编程语言的一部分,如:Lisp。在C#中引入拉姆计算符可以看做是C#迈向功能编程语言的第一步。
为了更详尽的介绍拉姆计算符,我们将从前面的示例开始,一步步的走向深入。现在,假设我们需要为显示的进程添加一个过滤器,即只显示满足条件的进程。比如,内存使用量大于20M的。通常,我们会这样来写:
static void DisplayProcessesSimple() { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { if (process.WorkingSet64 >= 20 * 1024 + 1024) { processes.Add(new ProcessData { Id = process.Id, Name = process.ProcessName, Memory = process.WorkingSet64 }); } } ObjectDumper.Write(processes); } |
使用委托
应该说,这没什么不对。但是如果考虑到通用性,即用户可以定义自己的过滤器,来决定是不是显示这个进程。那应该怎么办?首先想到的应该是委托。在.NET2.0中,为我们定义了这样的委托:
delegate Boolean Predicate<T>(T obj); |
即,对应这样的一个函数签名:根据传入的一个对象,返回True或者False来表示是否满足条件。由此,我们可以这样来定义一个函数:
static bool Filter(Process proc) { if (proc.WorkingSet64 >= 20 * 1024 * 1024) { return true; } return false; } |
该函数是满足Predicate委托的签名的。那么在DisplayProcessesSimple函数中,就可以做下面的修改了:
static void DisplayProcessesSimple(Predicate<Process> filter) { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { if (filter(process)) { processes.Add(new ProcessData { Id = process.Id, Name = process.ProcessName, Memory = process.WorkingSet64 }); } } ObjectDumper.Write(processes); } |
加入了参数。同时在判断的时候我们使用了指定的委托作为判断条件。在调用的时候也需要做一些修改,需要加入参数:
DisplayProcessesSimple(Filter); |
使用委托很好的解决了这个问题。但是,为了支持LINQ这还不够。接下来以同样的这个问题,看看我们怎么来简化这个操作。这个简化步骤正是LINQ的做法。
匿名方法
定义一个委托我们需要分几步。一是要定义个delegate来指定委托的签名。然后需要根据定义的委托签名写一个函数。最后就是在参数中加入委托的一个实例。前两步的目的都是为了在最后一步调用的时候更具有通用性。但是在做法上且不怎么直接。而且,在需要很多的使用这种委托的时候就显得很繁琐了。这个时候,匿名方法为我们提供了另外的一种更直接的解决办法。
使用匿名方法,我们可以直接的在参数中定义一个委托,改委托将作为参数在被调函数中使用。例如:
DisplayProcessesSimple( delegate(Process proc) { return proc.WorkingSet64 >= 20 * 1024 * 124; } ); |
这里,我们就可以省略Filter方法和委托的定义。直接在参数中写入一个匿名方法。这与前面介绍的隐式类型的变量和初始化器有异曲同工之妙。使用delegate(Process proc) 能自动的推导出委托的方法签名,即在使用委托时的参数。而大括号内的内容就相当于在定义的时候直接初始化了一个函数体。
因此,我们可以把匿名方法看做是委托的一个更直接的写法。但是,这还不够。如果在处理条件教复杂的时候这样的代码读起来还是让人觉得有点畏惧。下面就是拉姆计算符。
拉姆计算符
在详细介绍拉姆计算符以前,我们先看看拉姆计算符是怎么解决上面的问题的。下面的代码实现了与上面相同的效果:
DisplayProcessesSimple(process => process.WorkingSet64 >= 20 * 1024 * 1024); |
由此可见,拉姆计算符写的代码具有更强的可读性。针对这个例子,我们看看它是怎么实现这个效果的。其实,从在IL中,拉姆计算符和匿名方法的IL代码是一样的。也就是说他们的实现过程是一样的。按照隐式类型的变量的原则,在匿名方法中的参数类 型是可以省略的。因为编译器能根据调用的函数推导出参数类型。也就是说匿名方法并没有彻底的简化代码。这就是拉姆计算符需要做的。下面是拉姆计算符的解释: