C#语法扩展
在第一章中,我们讨论了为什么要使用LINQ并通过代码证明了使用LINQ我们能做哪些事。在这一章中,我们将要向你介绍为了支持LINQ我们的编程语言都做了哪些扩展。
C#和VB.NET都增加了新的内容以支持LINQ。我们觉得在进入LINQ的世界以前有必要了解.NET的新特征。这一章是
2.4 拉姆表达式(=>)
address => address.City == "Paris" |
在这一节中,我们将介绍另一个支持LINQ的新语言功能――拉姆表达式(=>)。拉姆表达式源自拉姆计算。许多功能性的编程语言都使用拉姆表达式定义函数。除了为了支持LINQ,引进拉姆表达式可以看作是.NET迈向功能性编程语言的一个重要步骤。
回到我们的例子。假设我们需要为我们的代码添加过滤功能,我们需要怎么做?当然,我们可以使用代理,将一个函数作为参数传给另一个。如下面将要看到的一样。
2.4.1 使用代理
对示例2.5中的代码,我们可以将过滤条件硬编码到代码中,如示例 2.6所示:
static void DisplayProccesses(Predicate<Process> match) { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { if (proc.WorkingSet64 >= 20 * 1024 * 1024) { processes.Add(new ProcessData { ID = process.Id, Memory = process.WorkingSet64, Name = process.ProcessName }); } } ObjectDumper.Write(processes); } |
WorkingSet64是系统为进城分配的内存总量。这里,我们要选择那些所用内存总量大于20M的进城。
为了让我们的程序更具可读性,我们试图将过滤条件作为参数传给DisplayProcesses方法。在C#2.0及以前版本,我们可以使用委托(Delegate)。委托是一种用来保存指向方法的指针的类型。
我们的过滤方法应该包含一个进程的实例作为参数,并且要返回一个Boolean类型来指示给定的进程是否满足过滤条件。可以按照这样来定义一个委托:
delegate Boolean FilterDelegate(Process process);
除了使用我们自定义的委托,我们也可以使用.NET2.0提供的一个委托:Predicate<T>。这是它的定义:
delegate Boolean Predicate<T>(T obj);
根据Predicate<T>委托所定义的方法将根据输入参数返回一个Ture或者False。由于给定的参数是泛型的,我们需要指定具体的类型,在这里是Process。因此,在这里,我们实际使用的委托类型是Predicate< Process >。示例 2-7展示了使用Predicate委托的DisplayProcesses函数:
static void DisplayProccesses(Predicate<Process> match) { var processes = new List<ProcessData>(); foreach (var process in Process.GetProcesses()) { if (match(process)) { processes.Add(new ProcessData { ID = process.Id, Memory = process.WorkingSet64, Name = process.ProcessName }); } } ObjectDumper.Write(processes); } static Boolean Filter(Process proc) { return proc.WorkingSet64 >= 20 * 1024 * 1024; } |
在上面的示例中,我们的Filter函数作为参数传给了DisplayProcesses函数。实现了与硬编码相同的效果。但很明显使用委托后的代码更具有可读性。
2.4.2 匿名函数
委托存在于C# 1.0中,但在C# 2.0中委托得到了扩张。它开始允许使用匿名方法。匿名方法可以减少代码量,同时避免使用显示命名的方法。
通过匿名方法,我们不用再显示的定义类似Filter的方法。可以直接将代码传给DisplayProcesses方法。如示例 2-9所示:
DisplayProcesses( delegate(Process process) { return process.WorkingSet64 >= 20 * 1024 * 1024; } ); 注意:需要注意的是VB.NET不支持匿名方法。 |
对那些熟悉C++标准模板库的读者可能会通过匿名函数联想到标准模板库中的functor。确实,匿名函数与functor很类似,它们都可以用一句话就处理集合中的元素。
在.NET2.0中,类型System.Collections.Generic.List<T>和System.Array下面的很多方法都进行了重新设计以支持匿名函数。这些方法包括Foreach、Find和FindAll。它们都可以使用很少的代码来操作集合中的元素。例如,下面的代码演示了Find方法调用匿名函数的效果:
var visualStudio = processes.Find( delegate(Process proc) { return proc.ProcessName == "devenv"; } ); |
2.4.3 关于拉姆表达式
除了像上面一样使用匿名函数外,从C# 3.0开始我们还可以使用拉姆表达式。示例 2-10中的代码与前面的代码实现的是完全相同的效果:
DisplayProccesses(process => process.WorkingSet64 >= 20 * 1024 * 1024); |
请注意拉姆表达式是如何简化代码的。上面的拉姆表达式可以读作“对于一个process对象,如果它的WorkingSet64大于20M就返回True”。从上面的代码我们可以发现,使用拉姆表达式,我们不用再提供参数的类型。这也印证了在C#的编译器能自动的识别作为函数参数的方法签名。
比较拉姆表达式和匿名方法
C# 2.0引入了匿名函数,使得我们可以直接将代码写在那些需要使用委托作为参数的地方。但是匿名函数的语法显得很冗长,而且是祈使式的。相反,拉姆表达式的语言就要简洁的多,并且在表现方式上也要更加丰富,更加具有可读性。拉姆表达式可以看作是匿名函数向功能性函数的一个扩展。它提供了以下的功能:
1. 拉姆表达式可以自动参照参数类型。因此,允许开发人员忽略参数类型。
2. 拉姆表达式的函数体可以是命令块,也可以是简单的表达式。为开发人员提供了灵活的选择。而匿名函数的函数体只能是命令块。
3. 在作为函数参数的时候,拉姆表达式能自动的识别参数类型,并使用相应的重载函数。而匿名函数只能自动识别返回参数的类型。
4. 对于使用表达式作为函数体的拉姆表达式,可以被转化为表达式树(我们将在下一章中介绍表达式树)。
拉姆表达式还引入了一些新的语法。在下一章中,我们将介绍这些语法,并通过一些示例来熟悉它们。
理解拉姆表达式
在C#中,拉姆表达式的格式一般由:参数列表,后面紧跟一个=>标记,然后是表达式或者命令模块。如 图 2-1所示:
注意:=>标记通常跟在参数列表后面。不要将=>与<=和>=混淆。
拉姆操作符可以读成“执行”。如果有参数,通常在操作符的左边。而右边就是要执行的表达式或者命令模块。
通常有两种拉姆表达式。如果拉姆操作符的右边是表达式的话就是表达式拉姆;而如果拉姆表达式的右边是由大括号扩起来的命令块的就是命令拉姆。为了更清楚的说明拉姆表达式,请看下面的例子:
1: |
X => X + 1; |
2: |
X => { return X + 1}; |
3: |
(int x) => x + 1; |
4: |
(int x) => { return x + 1; }; |
5: |
(x, y) => x * y; |
6: |
() => 1; |
7: |
() => Console.WriteLine() |
|
customer => customer.Name |
|
person => person.City == "Paris" |
|
(person, minAge) => person.Age >= minAge |
1. 隐式类型,表达式函数体。
2. 隐式类型,命令块函数体。
3. 显示类型,表达式函数体。
4. 显示类型,命令块函数体。
5. 多参数,隐式类型,表达式函数体。
6. 无参数,表达式函数体。
7. 无参数,命令块函数体。
注意:拉姆表达式的参数可以是显示定义了类型的,也可以是隐式类型的。
在VB.NET中,拉姆表达式的写法有所不同。它通常以Function关键字开始。如图 2-2所示:
示例2-12用VB.NET实现了我们上面C#的一些拉姆表达式:
1: |
Function(x) x+1 |
2: |
Function(x As Integer) x+1 |
3: |
Function(x, y) x*y |
4: |
Function() 1 |
|
Function(customer) customer.Name |
|
Function(person) person.City = “Paris” |
|
Function(person, miniAge) person.Age >= miniAge |
1. 隐式类型。
2. 显示类型定义。
3. 多参数。
4. 无参数。
从上面的例子可以看出,拉姆表达式和委托是兼容的。为了让你更深刻的认识到这一点,我们将会向你介绍一些委托的类型:
delegate void Action<T> (T obj);
delegate TOutput Converter<TInput, TOutPut >(TInput input);
delegate Predicate<T>(T obj);
另外一个非常有趣的委托在较早期的.NET版本就出现过,那就是MethodInvoker。这个委托的实例可是是任何没有参数和返回值的函数:
delegate void MethodInvoker();
非常遗憾的是MethodInvoker在以前的.NET版本中是定义在System.Windows.Forms的名称空间下面的,即使它在WinForm意外也有很广泛的使用。不过,在.NET3.5中,这有了改变。在.NET3.5中,通过在System.Core.dll中重载Action委托,实现了不带任何参数和返回值的委托:
delegate void Action();
注意:程序集System.Core.dll是.NET3.5才开始发布的。我们会在第三章中详细讨论它的和LINQ相关的内容。
除此之外,System.Core.dll中还添加了下面的委托类型:
delegate void Action<T1, T2>(T1 arg1, T2 Arg2);
delegate void Action<T1, T2, T3>(T1 arg1, T2 Arg2, T3 arg3);
delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 Arg2, T3 arg3, T4 arg4);
如果满足下面的条件,委托和拉姆表达式是可以通用的:
1. 拉姆表达式必须和委托具有相同数目的参数(译注:包括返回值)。
2. 每个拉姆表达式的参数类型必须可以隐含的转化为对应的委托的参数类型。
3. 如果有返回值,那么拉姆表达式的返回值必须可以隐含的转化为委托的返回值类型。
作为总结,我们为你准备了下面的示例。这些示例显示只要满足上面的条件,委托和拉姆表达式是可以相互转换的。效果也是一样的。
示例 2-13:定义为委托的拉姆表达式
1:Func (DateTime) getDateTime |
()=>DateTime.Now; |
2:Action<string> printImplicit |
s=>Console.WriteLine(s); |
3:Action<string> printExplicit |
(string s)=>Console.WriteLine(s); |
4:Func<int, int > SumInts |
(x, y) => x+y; |
5:Predicate<int> equalsOne1 |
x => x==1; |
Func<int, bool> equalsOne2 |
x => x==1; |
6:Func<int, int> IncInt |
x => x = x+1; |
Func<int, double> IncIntAsDouble |
x => x = x + 1; |
7:Func<int, int, int> Compare |
(int x, int y) => { if(x > y) return 1; if(x < y) return -1; return 0; } |
这是对上面的示例的说明:
1. 没有参数。
2. 隐含的string类型的参数。
3. 显示定义的string类型的参数。
4. 两个隐含类型的参数。
5. 效果一样,但并不兼容。
6. 同样的拉姆函数体,但是实现不同的委托。
7. 命令拉姆表达式,显示类型。
[译注:]拉姆表达式并不改变函数的定义。即,在该示例中,DisplayProcesses函数的参数还是一个委托Predicate<Process> match。在调用DisplayProcesses的时候只需要传入一个拉姆表达式就可以了。另外,拉姆表达式中,如果采用可以采用隐式和显示的参数类型定义。如果采用隐式,它会根据需要调用的函数的参数类型和列表自动的匹配参数类型。如果采用显示定义,就必须和被调函数的参数列表保持一致。
2.5 扩展方法(Extension Method)
static void Dump(this object o);
下面要介绍的是扩展方法(Extension Method)。扩展方法允许你向一个已经定义好的类型中添加方法。在下面的介绍中,你将会了解到扩张方法与静态方法的区别(Static)。
首先,我们将会向你介绍一个示例,然后引入扩展方法。最后,会向你介绍一些扩展方法的缺陷和使用的时候应该注意的地方。
2.5.1 创建一个简单的扩展方法
我们还是用前面的示例:列出所有的正在执行的进程。这次,我们需要计算所有的进程所使用的内存的总和。我们可以定义一个静态方法,然后为每个进程循环的调用该方法来计算内存的总和。如示例 2-15所示:
static Int64 TotalMemory(IEnumerable<ProcessData> processes) { Int64 result = 0; foreach (var process in processes) result += process.Memory; return result; } |
然后,可以用下面的方法调用:
Console.WriteLine("Total memory: {0} MB",TotalMemory(processes)/1024/1024);
在这里,我们可以将静态方法转化为扩展方法。这种新的特征使得我们在感觉上是为一个已经定义好了的类型添加了一个新的方法。
在C#中定义扩展方法
为了将方法转化为扩展方法,我们只需要为第一个参数添加this关键字。如示例 2-16所示:
static Int64 TotalMemory(this IEnumerable<ProcessData> processes) { Int64 result = 0; foreach (var process in processes) result += process.Memory; return result; } |
通过在第一个参数上面添加this关键字,编译器就会将改方法看做一个扩展方法,表明该方法是第一个参数的类型中的一个扩展。在这里,就是为已经定义好了的类型:IEnumerable<ProcessData>扩展了一个方法。
需要注意的是,在C#中,扩展方法只能定义在非泛型的静态类中。并且,扩展方法可以有任意的参数列表,但是第一个参数必须使用this关键字,表明接收该扩展方法的类型的参数。
接下来我们就可以把扩展方法看做是指定类型的一个成员,像下面一样调用它:
Console.WriteLine("Total memory: {0} MB", processes.TotalMemory() / 1024 / 1024); |
看起来就像是IEnumerable<ProcessData>有了一个新的成员TotalMemory一样。编译器会将上面的代码转化为一个静态方法。我们可以在VisualStudio的语法感应中看见
可能你也注意到了,TotalMemory方法和ToList还有ToLookUp方法有同样的图标。这说明它们都是扩展方法。实际上它们都是LINQ中非常有用的查询操作符,我们会在后面的章节介绍它们。
扩展方法的另一个非常有用的地方在于可以很容易的将多个操作连接在一起。假设有这样的情况:我们需要先过滤processes,然后计算内存总和,最后将计算的内存总和转化为M为单位。我们应该怎么做?实际上,使用扩展方法,我们可以很方便的实现下面的功能:
processes .FilterOutSomeProcesses() .TotalMemory() .BytesToMegaBytes(); |
需要注意的是,函数的执行顺序和调用顺序是一样的。
2.5.2 LINQ支持的其他查询操作符
LINQ为我们定义了一系列支持查询的扩展函数。这里我们只是简单的介绍几个,在后面的章节中我们会有其他的,更详细的介绍。
OrderByDesending
使用OrderByDesending方法,我们可以对集合进行排序。如:
var OrderedProcList = ProcList.OrderByDescending(process => process.Memory); |
这表明,我们需要按照占有内存大小对进程进行排序。需要注意的是参数的类型是编译器自动推算出来的。它会根据我们定义的ProcList(类型为IEnumerable<ProcessData>)自动的推算出OrderByDesending函数的参数是ProcessData。因此,我们不需要显示的定义需要处理的数据类型。OrderByDescending的定义如下:
public static IOrderedSequence<TSource> OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) |
隐藏类型的类
注意:在下面的情况下,编译器会将定义的两个隐藏类设为同一个实例:
1. 参数数目相同。
2. 参数顺序相同。
3. 参数的类型不一定要相同。