快乐的Lambda表达式(二)
自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜。它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能降低发生一些潜在错误的可能。LINQ包括ASP.NET MVC中的很多功能都是用Lambda实现的。我只能说自从用了Lambda,我腰也不酸了,腿也不疼了,手指也不抽筋了,就连写代码bug都少了。小伙伴们,你们今天用Lambda了么?但是你真的了解它么?今天我们就来好好的认识一下吧。
本文会介绍到一些Lambda的基础知识,然后会有一个小小的性能测试对比Lambda表达式和普通方法的性能,接着我们会通过IL来深入了解Lambda到底是什么,最后我们将用Lambda表达式来实现一些JavaScript里面比较常见的模式。
在.NET 1.0的时候,大家都知道我们经常用到的是委托。有了委托呢,我们就可以像传递变量一样的传递方法。在一定程序上来讲,委托是一种强类型的托管的方法指针,曾经也一时被我们用的那叫一个广泛呀,但是总的来说委托使用起来还是有一些繁琐。来看看使用一个委托一共要以下几个步骤:
复杂吗?好吧,也许06年你说不复杂,但是现在,真的挺复杂的。
后来,幸运的是.NET 2.0为了们带来了泛型。于是我们有了泛型类,泛型方法,更重要的是泛型委托。最终 在.NET3.5的时候,我们Microsoft的兄弟们终于意识到其实我们只需要2个泛型委托(使用了重载)就可以覆盖99%的使用场景了。
这样我们就可以跳过上面的第一步了,不过第2步还是必须的,只是用Action或者Func替换了。别忘了在.NET2.0的时候我们还有匿名方法,虽然它没怎么流行起来,但是我们也给它 一个露脸的机会。
Func<double, double> square = delegate (double x) { return x * x; }
最后,终于轮到我们的Lambda优雅的登场了。
// 编译器不知道后面到底是什么玩意,所以我们这里不能用var关键字 Action dummyLambda = () => { Console.WriteLine("Hello World from a Lambda expression!"); }; // double y = square(25); Func<double, double> square = x => x * x; // double z = product(9, 5); Func<double, double, double> product = (x, y) => x * y; // printProduct(9, 5); Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); }; // var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }); Func<double[], double[], double> dotProduct = (x, y) => { var dim = Math.Min(x.Length, y.Length); var sum = 0.0; for (var i = 0; i != dim; i++) sum += x[i] + y[i]; return sum; }; // var result = matrixVectorProductAsync(...); Func<double, double, Task<double>> matrixVectorProductAsync = async (x, y) => { var sum = 0.0; /* do some stuff using await ... */ return sum; };
从上面的代码中我们可以看出:
当然,关于最后一条,以下这些情况下我们还是可以用var关键字的。原因很简单,我们告诉编译器,后面是个什么类型就可以了。
Func<double,double> square = (double x) => x * x; Func<string,int> stringLengthSquare = (string s) => s.Length * s.Length; Action<decimal,string> squareAndOutput = (decimal x, string s) => { var sqz = x * x; Console.WriteLine("Information by {0}: the square of {1} is {2}.", s, x, sqz); };
现在,我们已经知道Lambda的一些基本用法了,如果仅仅就这些东西,那就不叫快乐的Lambda表达式了,让我们看看下面的代码。
var a = 5; Func<int,int> multiplyWith = x => x * a; var result1 = multiplyWith(10); //50 a = 10; var result2 = multiplyWith(10); //100
是不是有一点感觉了?我们可以在Lambda表达式中用到外面的变量,没错,也就是传说中的闭包啦。
void DoSomeStuff() { var coeff = 10; Func<int,int> compute = x => coeff * x; Action modifier = () => { coeff = 5; }; var result1 = DoMoreStuff(compute); ModifyStuff(modifier); var result2 = DoMoreStuff(compute); } int DoMoreStuff(Func<int,int> computer) { return computer(5); } void ModifyStuff(Action modifier) { modifier(); }
在上面的代码中,DoSomeStuff方法里面的变量coeff实际是由外部方法ModifyStuff修改的,也就是说ModifyStuff这个方法拥有了访问DoSomeStuff里面一个局部变量的能力。它是如何做到的?我们马上会说的J。当然,这个变量作用域的问题也是在使用闭包时应该注意的地方,稍有不慎就有可能会引发你想不到的后果。看看下面这个你就知道了。
var buttons = new Button[10]; for (var i = 0; i < buttons.Length; i++) { var button = new Button(); button.Text = (i + 1) + ". Button - Click for Index!"; button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); }; buttons[i] = button; }
猜猜你点击这些按钮的结果是什么?是”1, 2, 3…”。但是,其实真正的结果是全部都显示10。为什么?不明觉历了吧?那么如果避免这种情况呢?
var button = new Button(); var index = i; button.Text = (i + 1) + ". Button - Click for Index!"; button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); }; buttons[i] = button;
其实做法很简单,就是在for的循环里面把当前的i保存下来,那么每一个表达式里面存储的值就不一样了。
接下来,我们整点高级的货,和Lambda息息相关的表达式(Expression)。为什么说什么息息相关,因为我们可以用一个Expression将一个Lambda保存起来。并且允许我们在运行时去解释这个Lambda表达式。来看一下下面简单的代码:
Expression<Func<MyModel, int>> expr = model => model.MyProperty; var member = expr.Body as MemberExpression; var propertyName = member.Expression.Member.Name;
这个的确是Expression最简单的用法之一,我们用expr存储了后面的表达式。编译器会为我们生成表达式树,在表达式树中包括了一个元数据像参数的类型,名称还有方法体等等。在LINQ TO SQL中就是通过这种方法将我们设置的条件通过where扩展方法传递给后面的LINQ Provider进行解释的,而LINQ Provider解释的过程实际上就是将表达式树转换成SQL语句的过程。
关于Lambda性能的问题,我们首先可能会问它是比普通的方法快呢?还是慢呢?接下来我们就来一探究竟。首先我们通过一段代码来测试一下普通方法和Lambda表达 式之间的性能差异。
class StandardBenchmark : Benchmark { const int LENGTH = 100000; static double[] A; static double[] B; static void Init() { var r = new Random(); A = new double[LENGTH]; B = new double[LENGTH]; for (var i = 0; i < LENGTH; i++) { A[i] = r.NextDouble(); B[i] = r.NextDouble(); } } static long LambdaBenchmark() { Func<double> Perform = () => { var sum = 0.0; for (var i = 0; i < LENGTH; i++) sum += A[i] * B[i]; return sum; }; var iterations = new double[100]; var timing = new Stopwatch(); timing.Start(); for (var j = 0; j < iterations.Length; j++) iterations[j] = Perform(); timing.Stop(); Console.WriteLine("Time for Lambda-Benchmark: \t {0}ms", timing.ElapsedMilliseconds); return timing.ElapsedMilliseconds; } static long NormalBenchmark() { var iterations = new double[100]; var timing = new Stopwatch(); timing.Start(); for (var j = 0; j < iterations.Length; j++) iterations[j] = NormalPerform(); timing.Stop(); Console.WriteLine("Time for Normal-Benchmark: \t {0}ms", timing.ElapsedMilliseconds); return timing.ElapsedMilliseconds; } static double NormalPerform() { var sum = 0.0; for (var i = 0; i < LENGTH; i++) sum += A[i] * B[i]; return sum; } } }
代码很简单,我们通过执行同样的代码来比较,一个放在Lambda表达式里,一个放在普通的方法里面。通过4次测试得到如下结果:
Lambda Normal-Method
70ms 84ms
73ms 69ms
92ms 71ms
87ms 74ms
按理来说,Lambda应该是要比普通方法慢很小一点点的,但是不明白第一次的时候为什么Lambda会比普通方法还快一点。- -!不过通过这样的对比我想至少可以说明Lambda和普通方法之间的性能其实几乎是没有区别的。
那么Lambda在经过编译之后会变成什么样子呢?让LINQPad告诉你。
上图中的Lambda表达式是这样的:
Action<string> DoSomethingLambda = (s) => { Console.WriteLine(s);// + local };
对应的普通方法的写法是这样的:
void DoSomethingNormal(string s) { Console.WriteLine(s); }
上面两段代码生成的IL代码呢?是这样地:
DoSomethingNormal: IL_0000: nop IL_0001: ldarg.1 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret <Main>b__0: IL_0000: nop IL_0001: ldarg.0 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret
最大的不同就是方法的名称以及方法的使用而不是声明,声明实际上是一样的。通过上面的IL代码我们可以看出,这个表达式实际被编译器取了一个名称,同样被放在了当前的类里面。所以实际上,和我们调类里面的方法没有什么两样。下面这张图说明了这个编译的过程:
上面的代码中没有用到外部变量,接下来我们来看另外一个例子。
void Main() { int local = 5; Action<string> DoSomethingLambda = (s) => { Console.WriteLine(s + local); }; global = local; DoSomethingLambda("Test 1"); DoSomethingNormal("Test 2"); } int global; void DoSomethingNormal(string s) { Console.WriteLine(s + global); }
这次的IL代码会有什么不同么?
IL_0000: newobj UserQuery+<>c__DisplayClass1..ctor IL_0005: stloc.1 IL_0006: nop IL_0007: ldloc.1 IL_0008: ldc.i4.5 IL_0009: stfld UserQuery+<>c__DisplayClass1.local IL_000E: ldloc.1 IL_000F: ldftn UserQuery+<>c__DisplayClass1.<Main>b__0 IL_0015: newobj System.Action<System.String>..ctor IL_001A: stloc.0 IL_001B: ldarg.0 IL_001C: ldloc.1 IL_001D: ldfld UserQuery+<>c__DisplayClass1.local IL_0022: stfld UserQuery.global IL_0027: ldloc.0 IL_0028: ldstr "Test 1" IL_002D: callvirt System.Action<System.String>.Invoke IL_0032: nop IL_0033: ldarg.0 IL_0034: ldstr "Test 2" IL_0039: call UserQuery.DoSomethingNormal IL_003E: nop DoSomethingNormal: IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.0 IL_0003: ldfld UserQuery.global IL_0008: box System.Int32 IL_000D: call System.String.Concat IL_0012: call System.Console.WriteLine IL_0017: nop IL_0018: ret <>c__DisplayClass1.<Main>b__0: IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.0 IL_0003: ldfld UserQuery+<>c__DisplayClass1.local IL_0008: box System.Int32 IL_000D: call System.String.Concat IL_0012: call System.Console.WriteLine IL_0017: nop IL_0018: ret <>c__DisplayClass1..ctor: IL_0000: ldarg.0 IL_0001: call System.Object..ctor IL_0006: ret
你发现了吗?两个方法所编译出来的内容是一样的, DoSomtingNormal和<>c__DisplayClass1.<Main>b__0,它们里面的内容是一样的。但是最大的不一样,请注意了。当我们的Lambda表达式里面用到了外部变量的时候,编译器会为这个Lambda生成一个类,在这个类中包含了我们表达式方法。在使用这个Lambda表达式的地方呢,实际上是new了这个类的一个实例进行调用。这样的话,我们表达式里面的外部变量,也就是上面代码中用到的local实际上是以一个全局变量的身份存在于这个实例中的。
说到JavaScript,最近几年真是风声水起。不光可以应用所有我们软件工程现存的一些设计模式,并且由于它的灵活性,还有一些由于JavaScript特性而产生的模式。比如说模块化,立即执行方法体等。.NET由于是强类型编译型的语言,灵活性自然不如JavaScript,但是这并不意味着JavaScript能做的事情.NET就不能做,下面我们就来实现一些JavaScript中好玩的写法。
回调模式也并非JavaScript特有,其实在.NET1.0的时候,我们就可以用委托来实现回调了。但是今天我们要实现的回调可就不一样了。
void CreateTextBox() { var tb = new TextBox(); tb.IsReadOnly = true; tb.Text = "Please wait ..."; DoSomeStuff(() => { tb.Text = string.Empty; tb.IsReadOnly = false; }); } void DoSomeStuff(Action callback) { // Do some stuff - asynchronous would be helpful ... callback(); }
上面的代码中,我们在DoSomeStuff完成之后,再做一些事情。这种写法在JavaScript中是很常见的,jQuery中的Ajax的oncompleted, onsuccess不就是这样实现的么?又或者LINQ扩展方法中的foreach不也是这样的么?
我们在JavaScript中可以直接return一个方法,在.net中虽然不能直接返回方法,但是我们可以返回一个表达式。
Func<string, string> SayMyName(string language) { switch(language.ToLower()) { case "fr": return name => { return "Je m'appelle " + name + "."; }; case "de": return name => { return "Mein Name ist " + name + "."; }; default: return name => { return "My name is " + name + "."; }; } } void Main() { var lang = "de"; //Get language - e.g. by current OS settings var smn = SayMyName(lang); var name = Console.ReadLine(); var sentence = smn(name); Console.WriteLine(sentence); }
是不是有一种策略模式的感觉?这还不够完美,这一堆的switch case看着就心烦,让我们用Dictionary<TKey,TValue>来简化它。来看看来面这货:
static class Translations { static readonly Dictionary<string, Func<string, string>> smnFunctions = new Dictionary<string, Func<string, string>>(); static Translations() { smnFunctions.Add("fr", name => "Je m'appelle " + name + "."); smnFunctions.Add("de", name => "Mein Name ist " + name + "."); smnFunctions.Add("en", name => "My name is " + name + "."); } public static Func<string, string> GetSayMyName(string language) { //Check if the language is available has been omitted on purpose return smnFunctions[language]; } }
自定义型方法在JavaScript中比较常见,主要实现思路是这个方法被设置成一个属性。在给这个属性附值,甚至执行过程中我们可以随时更改这个属性的指向,从而达到改变这个方法的目地。
class SomeClass { public Func<int> NextPrime { get; private set; } int prime; public SomeClass { NextPrime = () => { prime = 2; NextPrime = () => {
// 这里可以加上 第二次和第二次以后执行NextPrive()的逻辑代码 return prime; }; return prime; } } }
上面的代码中当NextPrime第一次被调用的时候是2,与此同时,我们更改了NextPrime,我们可以把它指向另外的方法,和JavaScrtip的灵活性比起来也不差吧?如果你还不满意 ,那下面的代码应该能满足你。
Action<int> loopBody = i => { if(i == 1000) loopBody = //把loopBody指向别的方法 /* 前10000次执行下面的代码 */ }; for(int j = 0; j < 10000000; j++) loopBody(j);
在调用的地方我们不用考虑太多,然后这个方法本身就具有调优性了。我们原来的做法可能是在判断i==1000之后直接写上相应的代码,那么和现在的把该方法指向另外一个方法有什么区别呢?
JavaScript 中的自执行方法有以下几个优势:
在C#中我们也可以有自执行的方法:
(() => { // Do Something here! })();
上面的是没有参数的,如果你想要加入参数,也非常的简单:
((string s, int no) => { // Do Something here! })("Example", 8);
.NET4.5最闪的新功能是什么?async?这里也可以
await (async (string s, int no) => { // 用Task异步执行这里的代码 })("Example", 8); // 异步Task执行完之后的代码
大家知道.NET为我们提供了匿名对象,这使用我们可以像在JavaScript里面一样随意的创建我们想要对象。但是别忘了,JavaScript里面可以不仅可以放入数据,还可以放入方法,.NET可以么?要相信,Microsoft不会让我们失望的。
//Create anonymous object var person = new { Name = "Jesse", Age = 28, Ask = (string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42!"); } }; //Execute function person.Ask("Why are you doing this?");
但是如果你真的是运行这段代码,是会抛出异常的。问题就在这里,Lambda表达式是不允许赋值给匿名对象的。但是委托可以,所以在这里我们只需要告诉编译器,我是一个什么类型的委托即可。
var person = new { Name = "Florian", Age = 28, Ask = (Action<string>)((string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42!"); }) };
但是这里还有一个问题,如果我想在Ask方法里面去访问person的某一个属性,可以么?
var person = new { Name = "Jesse", Age = 18, Ask = ((Action<string>)((string question) => { Console.WriteLine("The answer to '" + question + "' is certainly 20. My age is " + person.Age ); })) };
结果是连编译都通不过,因为person在我们的Lambda表达式这里还是没有定义的,当然不允许使用了,但是在JavaScript里面是没有问题的,怎么办呢?.NET能行么?当然行,既然它要提前定义,我们就提前定义好了。
dynamic person = null; person = new { Name = "Jesse", Age = 28, Ask = (Action<string>)((string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42! My age is " + person.Age + "."); }) }; //Execute function person.Ask("Why are you doing this?");
这个模式和自定义型方法有点类似,唯一的不同是它不是在定义自己,而是在定义别的方法。当然,只有当这个方法基于属性定义的时候才有这种实现的可能。
public Action AutoSave { get; private set; } public void ReadSettings(Settings settings) { /* Read some settings of the user */ if(settings.EnableAutoSave) AutoSave = () => { /* Perform Auto Save */ }; else AutoSave = () => { }; //Just do nothing! }
可能有人会觉得这个没什么,但是仔细想想,你在外面只需要调用AutoSave就可以了,其它的都不用管。而这个AutoSave,也不用每次执行的时候都需要去检查配置文件了。
Lambda表达式在最后编译之后实质是一个方法,而我们声明Lambda表达式呢实质上是以委托的形式传递的。当然我们还可以通过泛型表达式Expression来传递。通过Lambda表达式形成闭包,可以做很多事情,但是有一些用法现在还存在争议,本文只是做一个概述 :),如果有不妥,还请拍砖。谢谢支持 :)
还有更多Lambda表达式的新鲜玩法,请移步: 背后的故事之 - 快乐的Lambda表达式(二)
原文链接: http://www.codeproject.com/Articles/507985/Way-to-Lambda