在C# 3.0 中,Microsoft 添加了“lambda 表达式”。lamdba表达式曾经用于很久以前的LISP计算机语言中,在1936年由一个美国数学家Alonzo Church对其进行了概念化描述。这些表达式提供了便捷的语法来指定一个算法。
但是在接下来开始介绍lambda表达式之前,首先看看将一个算法指定为某个方法的参数的演进过程,因为这正是lambda表达式的目的。
在C# 2.0之前,当一个方法或变量要用到委托(delegate)时,开发人员必须创建一个命名方法,并在需要委托的位置传入这个名称。例如,考虑以下情况。
假定有两个开发者,一个是通用代码开发者,而另一个是应用程序开发者。但是,实际上没有必要的有两个不同的开发者,只需要通过标记来描述两个不同的角色即可。通用代码开发者需要创建通用目的代码,这些代码可以在整个工程中被重复使用。应用程序开发者将使用这些通用目的的代码来创建一个应用程序。
在这个示例的场景中,通用代码开发者想要创建一个通用方法来筛选整形数组,但是这个通用方法要能够指定用来筛选该数组的算法。首先,开发者必须声明一个委托 delegate,该委托的设计原型就是收到一个int类型,如果在被筛选的数组中确实包含一个int类型,则这个委托返回true。
于是,开发者创建了一个工具类并添加了 delegate 和筛选方法。这个公告代码如下所示:
public class Common { public delegate bool IntFilter(int i); public static int[] FilterArrayOfInts(int[] ints, IntFilter filter) { ArrayList aList = new ArrayList(); foreach (int i in ints) { if (filter(i)) { aList.Add(i); } } return ((int[])aList.ToArray(typeof(int))); } }
这个公共代码的开发者将把 delegate 声明和 FilterArrayOfInts 方法放到一个公共的库程序集中,也就是动态链接库(DLL)中,这样就可以在多个应用程序中使用这个声明和方法了。
上面列出的 FilterArrayOfInts 方法允许应用程序开发者将一个整形数组和一个 delegate 作为参数传递到筛选方法中,并将获得一个筛选后的数组。
现在,假定应用程序开发者只想筛选奇数。则可以参见下面的筛选方法,该方法在开发者的应用程序代码中进行了声明。
public class Application { public static bool IsOdd(int i) { return ((i & 1) == 1); } }
根据 FilterArrayOfInts 方法中的代码,传入数组中的每个 int 整形都会调用这个方法。如果传送的 int 整形数是奇数,则此筛选方法将只返回true。下面是使用示例:
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, Application.IsOdd); foreach (int i in oddNums) { Console.WriteLine(i); }
示例的运行结果如下:
请注意,要想将 delegate 传递为 FilterArrayOfInts 方法的第二个参数,应用程序开发者只需要传递该方法的名称。只需要简单的创建另一个筛选器,应用程序开发者可以筛选不同的数据。应用程序开发者还可以为偶数、素数,以及任何想要的规则创建筛选器。方法委托(delegate)可以通过这种方式成为具有很高可重用的代码。
上面介绍的方法是很用效而且很好的,但是为所有这些筛选方法和所需的任何其他方法委托编写代码是很单调乏味的。在这些方法中,实际上许多方法都只用于单次调用,因此为所有这些方法都创建命名方法是很麻烦的。从C# 2.0开始,开发者可以使用匿名方法来传递内联代码,也就是说,用内联代码来取代方法委托。匿名方法允许开发者在通常用来传递方法委托的位置指定一段筛选代码即可。示例如下:
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums , delegate(int i) { return ((i & 1) == 1); }); foreach (int i in oddNums) { Console.WriteLine(i); } Console.Read();
这样做真的很酷。应用程序开发者不再需要在任何位置声明一个方法。这样做对于将来不需要被重新使用的筛选逻辑代码而言是非常好的。正如代码要求的那样,该示例的运行结果与上一个示例相同。
lambda 表达式可以写成一个逗号分隔的参数列表,后面加一个 lamdba 操作符,在后面是一个表达式或声明函数。如果有多个输入参数,则使用圆括号包含输入参数。在 C# 中,lambda 操作符可以写为“=>”。这样,C# 中的 lambda表达式可以写成:
(param1, param2, ...paramN) => expr
而在需要用到更复杂的表达式时,可以使用以下函数声明:
(param1, param2, ...paramN) => { statement1; statement2; ... statementN; return(lambda_expression_return_type); }
在这个示例中,这个函数声明最后放回的数据类型必须匹配 delegate 指定的放回类型。
下面是 lambda 表达式的有一个简单示例:
x => x
这个 lambda 表达式可以读做“ x 到 x ”,或者读做“输入x返回x”。这意味着对于输入变量x,将会返回x。这个表达式只是用于返回传入的参数。因为该表达式只用一个输入参数x,因此其不必包含在圆括号中。非常重要的是,要知道是 delegate 在指示输入参数x的类型,以及要返回的值的类型。例如,如果 delegate 被定义为传入一个string类型的参数,返回一个bool型值,则不能使用x=>x表达式,这是因为如果输入的参数x 是一个string类型,则返回的值同样也应该是string,但是,delegate 指定该返回值必须为 bool 类型。因此,对于被定义为那样的delegate,表达式中位于lambda操作符(=>)右边的部分必须等于或返回一个bool型值,比如:
x => x.Length > 0
这个lambda表达式可以读做“x到x.Length>0”,或则读做“输入x返回x.Length > 0”。由于这个表达式的右边部分等于一个bool型值,则 delegate 应该指定该方法将返回一个bool型值,否则将会编译错误。
下面的 lambda 表达式将返回输入参数的长度。因此 delegate 应该指定一个int 型返回值:
s => s.Length
如果要将多个参数传入到lambda表达式中,可以使用逗号分隔这些参数,并将这些参数包含在圆括号中,例如:
(x, y) => x == y
复杂的 lambda 表达式甚至要使用函数声明,例如:
(x, y) => { if (x > y) return x; else return y; }
重点要记住的是,delegate 定义了输入什么类型的值和必须返回什么类型的值。因此请确保 lambda 表达式匹配 delegate的定义。
警告: 请确认编写的 lambda 表达式满足 delegate 定义指定的输入类型,并返回 delegate 定义的返回类型。
要想记得更清楚一点,可以参考下面的公告代码开发者定义的 delegate 语句:
public delegate bool IntFilter(int i);
应用程序开发者的lambda表达式必须支持传入一个int型参数,并返回一个bool型值。这可以通过该表达式调用的方法和筛选方法的功能来进行推断,但是,重点要记住的是,这是由 delegate 委托规定的。
如果使用 lambda 表达式,则上面的实例可以写成这样:
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oddNums = Common.FilterArrayOfInts(nums, i => ((i & 1) == 1)); foreach (int i in oddNums) { Console.WriteLine(i); }
的确,真是很简洁的代码。这段代码看起来很有趣,因为这些代码非常新,但是在习惯使用这样写代码后,可以确信这些代码是可读和可维护的。正如想要的那种,这段代码的运行结果与上一个示例的运行结构相同:
回顾一下,下面列出了每种方法的示例代码中的关键代码行:
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //1.使用命名方法 int[] oddNums = Common.FilterArrayOfInts(nums, Application.IsOdd); //2.使用匿名方法 int[] oddNums = Common.FilterArrayOfInts(nums , delegate(int i) { return ((i & 1) == 1); }); //3.使用lambda表达式 int[] oddNums = Common.FilterArrayOfInts(nums, i => ((i & 1) == 1));
从上面的代码可以看出,第一个方法的代码行更短一些,但是不要忘记,在代码的其他位置还必须声明一个命名方法来具体定义该方法的功能。当然,如果将来在很多位置都会重新使用这个筛选逻辑,或者这个方法的算法很复杂,并且必须