从** C#3.0开始,可以使用一种新的方法把实现代码赋予委托: Lambda表达式**。只要有委托参数类型的地方,就可以使用Lambda表达式。
这是前面用到的一个Lambda表达式
myTimer.Elapsed += (sender, eventArgs) =>
{
Console.Write(displayString[counter++ % displayString.Length]);
};
lambda运算符
=>
的左边列出了需要的参数,右边定义了赋予lambda变量的方法的实现代码。
参数
lambda表达式有几种定义参数的方式。如果只有一个参数,只需写出参数名。
下面的例子使用参数s
,因为委托定义了一个string
参数,那么s
的类型也就是string
。实现代码调用String.Format()
方法来返回一个字符串,在调用这个委托时,字符串就打印出来。
Func<string,string> oneparam = s => String.Format("变为大写 {0}", s.ToUpper());
Console.WriteLine(oneparam("text"));
如果委托有多个参数,就把参数名放在()
里面。例如:
Func<double,double,double> twoparams = (x, y) => x * y;
Console.WriteLine(twoparams(3, 2));
这里x
,y
的类型是double
,由Func<double,double,double>
委托定义.
上面的例子里都没有给出参数的类型,你可以给变量名添加参数类型。如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托:
Func<double,double,double> twoparams = (double x, double y) => x * y;
Console.WriteLine(twoparams(3, 2));
多行代码
如果lambda表达式只有一条语句,在方法块内就不需要{}
和return
语句,编译器会隐式添加一条return
。
Func<double, double> squre = x => x * x;
添加{}
和return
可以让代码更加易读。
Func<double, double> squre = x => {
return x * x;
}
但是如果要在lambda表达式中添加更多语句,就必须使用{}
和return
Func<string, string> lambda = param =>{
param += mid;
param += "and this was added to the string.";
return param;
};
闭包
通过lambda表达式可以访问表达式块外部的变量。这称为闭包。但使用时需要注意。
int val = 5;
Func<int, int> f = x => x + val;
Func<int, int>
类型的lambda表达式需要一个int
参数,返回一个int
,代码访问了外部的val
变量。调用的结果应该是x+5
,但是实际上会更复杂一些。
要是在以后会修改val
的值,再次调用这个lambda表达式时,会使用val
的新值。
如果有一个线程调用这个lambda表达式,我们可能就会不知道结果到底是多少。
对于表达式x => x + val
编译器会创建一个匿名类,有一个构造方法来接收参数,另一个方法实现并返回结果。
foreach的闭包
针对闭包,C#5.0
中的foreach
语句有了很大改变。
var values = new List<int>(){ 10, 20, 30};
var funcs = new List<Func<int>>();
foreach (var val in values){
funcs.Add(() => val);
}
foreach (var f in funcs){
Console.WriteLine((f()));
}
这段代码
funcs
泛型列表中添加lambda表达式,第二条foreach
语句迭代输出列表中引用的每个函数。其实每个函数都返回一个List<int>
列表中的数字。
在C#4.0
或更早的版本中,会输出30
三次,而不是迭代时获得的val
变量。这个foreach
的内部实现有关。编译器会从foreach
语句创建一个while
循环。在C#4.0
中,编译器在while
循环外部定义循环变量,每次迭代中重用这个变量。因此,在循环结束时,该变量的值是最后一次迭代的值。要在C#4.0
中得到我们希望的结果需要在第一个foreach
做如下操作:
var v = val;
funcs.Add(() => v);
在C#5.0
中不需要再这样,代码会修改为局部变量。
Lambda表达式用于匿名方法
Lambda表达式是简化匿名方法的一种方式。本文就是以这个lambda表达式开始的。
编译器会提取这个lambda表达式,创建一个匿名方法,工作方式匿名方法相同。其实它会被编译成相同或相似的CIL代码。
下面举一个书上的栗子,我有扩展。
这是一个委托定义,表示一个方法,有两个int
参数,返回一个int
结果。
private delegate int twoparams(int p1, int p2);
这是一个以上面委托为参数的方法。
static void Perform(twoparams tdel)
{
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 5; j++)
{
int result = tdel(i, j);
Console.Write("f({0},{1})={2}", i, j, result);
if (j != 5)
Console.Write(" ,");
}
Console.WriteLine();
}
}
可以给这个方法传一个委托实例,也可以是匿名方法,lambda表达式。
为什么可以是匿名方法,lambda表达式?这是因为这些结构都会被编译为委托实例。
这个方法会用一组值调用委托实例所表示的方法,并把参数输出。
下面我创建一个方法来调用作为示例。
static void Show()
{
twoparams test;
test = Tdel;
Console.WriteLine("a+b");
Perform(((p1, p2) => p1 + p2));
Console.WriteLine("a*b");
Perform((
delegate (int p1, int p2){ return p1 * p2; }
));
Console.WriteLine("2*a*b");
Perform(Tdel);
Console.WriteLine("2*a*b-22222");//22222纯属为了方便区分,在IL中查看
Perform(test);
}
private static int Tdel(int p1, int p2)
{
return p1*p2*2;
}
这里用了4种方式来调用:
-
Perform(((p1, p2) => p1 + p2));
使用lambda表达式; -
Perform((delegate (int p1, int p2){ return p1 * p2; }));
使用匿名函数; -
Perform(Tdel);
给方法传递一个匹配委托的方法,似乎一个方法不是一个委托,但因为其满足委托的签名,是可行的,编译器同样可以将其编译成一个委托实例。 - Perform(test);这是这个实例中唯一一个满足方法参数的,
twoparams test;
创建一个委托实例,并给它提供一个方法test = Tdel;
。
主函数中运行一下,得到如下结果:
用4种方法调用均可行,得到预期结果。为了验证它会被编译成相同或相似的CIL代码,我们来看看Show
这个方法的中间代码。
private static void Show()
{
Program.twoparams tdel = new Program.twoparams(Program.Tdel);
Console.WriteLine("a+b");
Program.twoparams arg_38_0;
if ((arg_38_0 = Program.<>c.<>9__4_0) == null)
{
arg_38_0 = (Program.<>c.<>9__4_0 = new Program.twoparams(Program.<>c.<>9.<Show>b__4_0));
}
Program.Perform(arg_38_0);
Console.WriteLine("a*b");
Program.twoparams arg_68_0;
if ((arg_68_0 = Program.<>c.<>9__4_1) == null)
{
arg_68_0 = (Program.<>c.<>9__4_1 = new Program.twoparams(Program.<>c.<>9.<Show>b__4_1));
}
Program.Perform(arg_68_0);
Console.WriteLine("2*a*b");
Program.Perform(new Program.twoparams(Program.Tdel));
Console.WriteLine("2*a*b-22222");
Program.Perform(tdel);
}
可以看到,使用lambda表达式和使用匿名方法得到的中间代码非常像。最终还是给方法传递了一个
twoparams
的委托参数。而给一个符合委托参数的方法作为参数得到的中间代码Program.Perform(new Program.twoparams(Program.Tdel));
同样如此,可以看到,我们给的是方法作为参数,编译器编译成为委托,并且为这个委托指定了我们给的方法名。一切仿佛都变清楚了。