c#3.0新特性(二):Lambda表达式

一、由一个简单的排序想到的
在javascript中,数组对象Array有一个sort方法,在javascript:内置对象学习笔记二 我已经整理总结了一下,现在拿出来重温一遍:
Code
也可以这么写:
Code
js的排序简单直观,在c#里,也可以实现类似的排序,而且更强大和方便,比如c#中的泛型排序方法,常见的List<T>就有如下Sort方法:
public void Sort();
public void Sort(Comparison<T> comparison);
public void Sort(IComparer<T> comparer);
public void Sort(int index, int count, IComparer<T> comparer);
现以List<T>举一个应用实例:
Code
我们还可以用下面的匿名方法代替上面的BookComparasion类的方法(虽然我不喜欢匿名方法):
Code
下面的代码可以轻松实现相同的功能:
Code
上面的代码注释已经写的很清楚,没错,现在向“主流”靠拢,就是Lambda表达式了。
二、Lambda表达式学习笔记
在C#2.0中引入了匿名方法允许在期望出现委托的时候以“内联”的代码替代之。尽管匿名方法提供了函数式编程语言中的很多表达能力,但匿名方法的语法实在是太“不近人情”了,并且很不自然,用多了会造成阅读上的困难。举例来说:
Code
上面的匿名委托我们可以简化为Lambda表达式来表示。实际上Lambda表达式的本质是匿名方法,也即是当编译我们的程序代码时,编译器会自动帮我们将Lambda表达式转换为匿名方法。看下改进后的代码:
下面系统学习Lambda表达式的相关语法和应用:
1、创建
Lambda表达式的书写方式是一个参数列表后跟“=>”记号,然后跟一个表达式或一个语句块,即Lambda表达式的语法格式为:参数列 => 语句或语句块。
(param1, param2, …paramN) =>
{
statement1;
statement2;

statementN;
return(lambda_expression_return_type);

2、关于“参数列

Lambda表达式的参数列可以具有显式的或隐式的类型。在一个具有显式类型的参数列表中,每个参数的类型都是显式声明的。在一个具有隐式类型的参数列表中,参数的类型是从Lambda表达式出现的上下文中推断出来的——具体来说,是当Lambda表达式被转换为一个兼容的委托类型时,该委托类型提供了参数的类型。
当Lambda表达式只有一个具有隐式类型的参数时,参数列表中的括号可以省略。即:(param) => expression可以简写为:param => expression。
注意,参数列中可包含任意个参数(与委托对应),如果参数列中有0个或1个以上参数,则必须使用括号括住参数列,举例如下:
() => Console.Write("0个参数");

i => Console.Write("1个参数时参数列中可省略括号,值为:{0}", i);

(x, y) => Console.Write("包含2个参数,值为:{0}:{1}", x, y);

而“语句或语句块”中如果只有一条语句,则可以不用大括号括住,否则则必须使用大括号,如下所示:

 //只有一条语句,则可以不用大括号括住
 i => Console.Write("只有一条语句");

 i => { Console.Write("使用大括号的表达式"); };

 //两条语句时必须要大括号
 i => { i++; Console.Write("两条语句的情况"); };
如果“语句或语句块”有返回值时,如果只有一条语句则可以不写“return”语句,编译器会自动处理,否则必须加上,如下示例:

Code
3、遵循规则

Lambda表达式是委托的实现方法,所以必须遵循以下规则:
(1)Lambda表达式的参数数量必须和委托的参数数量相同;
2)如果委托的参数中包括有ref或out修饰符,则Lambda表达式的参数列中也必须包括有修饰符;
(3)如果委托有返回类型,则Lambda表达式的语句或语句块中也必须返回相同类型的数据;
4)如果委托有几种数据类型格式而在Lambda表达式中编译器无法推断具体数据类型时,则必须手动明确数据类型。
示例如下:

Code

 小结: C# 2.0规范中提到的匿名方法规范同样适用于Lambda表达式。Lambda表达式是匿名方法在功能行上的超集,提供了下列附加的功能:

(1)Lambda表达式允许省略参数类型并对其进行推断,而匿名方法要求参数类型必须显式地声明。
(2)Lambda表达式体可以是表达式或语句块,而匿名方法体只能是语句块。
(3)在类型参数推导和方法重载抉择时,Lambda表达式可以被作为参数传递。
4)以一个表达式作为表达式体的Lambda表达式可以被转换为表达式树
4、Lambda表达式的转换
两个代表:下面行文中的D代表Delegate(委托类型),L代表Lambda表达式
和匿名方法表达式类似,Lambda表达式可以归类为一种拥有特定转换规则的值。这种值没有类型,但可以被隐式地转换为一个兼容的委托类型。
特别地,当满足下列条件时,委托类型兼容于Lambda表达式:
(1) D和L具有相同数量的参数;
(2) 如果L具有显式类型的参数列表,D中每个参数的类型和修饰符必须和L中相应的参数完全一致;
(3) 如果L具有隐式类型的参数列表,则D中不能有ref或out参数;
(4) 如果D具有void返回值类型,并且L的表达式体是一个表达式,若L的每个参数的类型与D的参数一致,则L的表达式体必须是一个可接受为statement-expression的有效表达式;
(5) 如果D具有void返回值类型,并且L的表达式体是一个语句块,若L的每个参数的类型与D的参数一致,则L的表达式体必须是一个有效语句块,并且该语句块中不能有带有表达式的return语句;
(6) 如果D的返回值类型不是void,并且L的表达式体是一个表达式,若L的每个参数的类型与D的参数一致,则L的表达式体必须是一个可以隐式转换为D的返回值类型的有效表达式;
(7) 如果D的返回值类型不是void,并且L的表达式体是一个语句块,若L的每个参数的类型与D的参数一致,则L的表达式体必须是一个有效的语句块,该语句块不能有可达的终点
(即必须有return语句),并且每个return语句中的表达式都必须能够隐式转换为D的返回值类型。
(ps:ms这里的约束也太多了吧^_^。哎,不管了,先抄下来,以后会熟练运用就行了)

三、一个需要注意的地方
看下面的代码:

Code

运行试一下看看结果,如果你以前碰到过js里的闭包问题,相信你不会大惊小怪(而且可能已经知道了问题的原因),但是,如果你从来没有碰到过这种情况,是不是令你大吃一惊?!输出的竟然不是0,1,2,而是三个3,oh,my god。紧接着,立刻,你会大胆想到这里的list在Add方法执行的地方Add进去的是一个引用类型(这里是lambda表达式()=>i),它们执行的结果共同指向值为3的同一个引用地址!
没错,我们详细分析一下:
1、我们首先定义一个list,其存储格式为func<int>,即返回int型的代理;然后,用for循环将i封装进lambda表达式,并加入到该list中,最后,用foreach循环输出结果。
2、因为lambda表达式实质就是个委托,也就指向一个匿名函数,所以,在foreach输出的时候,使用item()来调用它,让它所指向的函数执行。
至于第2步中item()执行的结果为什么都是3,原因是这样的:
(1)在for循环中,只能有一个 i 变量。即在第一次循环时,i 的地址就分配好了(注意了,这里i的地址第一次分配后是不变的),不会因为循环次数的多少而发生任何改变,其改变的只能是里面装载的值。
(2)lambda表达式在构造时, 传进去的是变量的地址,而不是具体值。只有当真正执行这个lambda表达式时,才会去确定它的值。这就是为什么上面的例子中,其结果均为3(for循环在最后,当i=2时,i又加了1)。

那么如何解决这个问题?解决方案很简单,就是在for循环中,定义一临时变量tmpNum,存储i的值即可。因为编译器会对该临时变量重新分配内存,这样,每次循环,都重新分配新的内存,就不会有这个问题了。
最后把这个“功德圆满”的解决方案贴出来:

Code

好了,Lambda表达式先学到这里,多读多写熟练就不害怕了,继续学习去也。

Code

posted on 2009-06-01 22:17  JeffWong  阅读(1145)  评论(1编辑  收藏  举报