Javascript闭包和C#匿名函数对比分析
C#中引入匿名函数,多少都是受到Javascript的闭包语法和面向函数编程语言的影响。人们发现,在表达式中直接编写函数代码是一种普遍存在的需求,这种语法将比那种必须在某个特定地方定义函数的方式灵活和高效很多,比如回调和事件处理都特别适合使用表达式中直接编写函数的形式,因此C#的匿名函数也就应运而生。
初识C#中的匿名函数,多多少少并不是那么直观,在匿名函数中,可以直接使用该匿名函数所在的函数中的局部变量,这和Javascript闭包函数在语法形式和运行结果上非常相似,但两者在实现原理上却完全不同,后者是语言内在特性,而前者(C#匿名函数)只是一个编译器功能,也称语法糖。
1. Javascript闭包
作者在《Javascript本质第一篇:核心概念》和《Javascript本质第二篇:执行上下文》这两篇文章中对Javascript的核心特性——包括执行上下文——做了详细介绍。很多概念都给出了明确定义,似乎缺少了闭包。
先列出一段代码,明确这里关于父子函数的定义,作为参考:
1 function funParent() { // 父函数 2 var v = "parent funtion's variable"; 3 function funChild() { //子函数 4 return v; 5 }; 6 return funChild; 7 } 8 fun = funParent();
Javascript中闭包的定义:
闭包就是函数,函数就是闭包。
在作用域的角度上,将函数称为闭包。
通常在以下场景中我们更趋向于突出一个函数的闭包的概念:一个函数在其函数体中使用了定义该函数的父函数中的var变量,而且这个函数在父函数之外被使用。
例如在上面的代码的中,我们通常将函数fun叫做闭包,而不去刻意突出函数funParent的闭包的概念。
如将上面的代码改为:
1 function funParent() { // 父函数 2 var v = "parent funtion's variable"; 3 function funChild() { //子函数 4 return v; 5 }; 6 funChild(); 7 } 8 funParent();
这个时候的funChild函数和前面的示例代码中的funChild或fun在内在结构和行为上没有任何的区别,只是函数及其所引用的执行上下文被释放的时机的问题。
2. C#的匿名函数
C#中可以通过lambda表达式的形式在函数中定义匿名函数:
(参数) => {代码}
在匿名函数的代码中可以使用定义该匿名函数的函数中的局部变量,这一特性与Javascript中函数中的函数一样。
下面的代码在Init函数中定义两级嵌套的匿名函数:
1 namespace ConsoleTest1 2 { 3 class A 4 { 5 public Action Show; 6 public Action<string> Set; 7 public Action ShowNested; 8 public Action<string> SetNested; 9 public void Init() 10 { 11 string str = "你好"; 12 this.Show += () => 13 { 14 Console.WriteLine(str + "!"); 15 string name = "张三"; 16 ShowNested = () => 17 { 18 Console.WriteLine(str + "," + name + "!"); 19 }; 20 SetNested = (v) => 21 { 22 name = v; 23 }; 24 }; 25 this.Set += (v) => 26 { 27 str = v; 28 }; 29 } 30 }; 31 32 class Program 33 { 34 static void Main(string[] args) 35 { 36 var a = new A(); 37 a.Init(); 38 39 a.Show(); 40 a.Set("Hello"); 41 a.Show(); 42 43 a.ShowNested(); 44 a.SetNested("Zhang San"); 45 a.ShowNested(); 46 } 47 } 48 }
运行结果如下:
你好!
Hello!
Hello,张三!
Hello,Zhang San!
请按任意键继续. . .
匿名函数的运行行为与Javascript中闭包函数的运行行为相似。
但是,在C#中,name和str明显不符合局部变量的行为特性,通过反编译生成的exe文件,可以看到,Init函数已经被编译器完全重构,专门的类被创建,来封装name和str变量,实现匿名函数。匿名函数最终还是由有名函数实现。
反编译结果如下:
1 namespace ConsoleTest1 2 { 3 internal class A 4 { 5 [CompilerGenerated] 6 private sealed class <>c__DisplayClass4 7 { 8 private sealed class <>c__DisplayClass6 9 { 10 public A.<>c__DisplayClass4 CS$<>8__locals5; 11 public string name; 12 public void <Init>b__1() 13 { 14 Console.WriteLine(this.CS$<>8__locals5.str + "," + this.name + "!"); 15 } 16 public void <Init>b__2(string v) 17 { 18 this.name = v; 19 } 20 } 21 public string str; 22 public A <>4__this; 23 public void <Init>b__0() 24 { 25 A.<>c__DisplayClass4.<>c__DisplayClass6 <>c__DisplayClass = new A.<>c__DisplayClass4.<>c__DisplayClass6(); 26 <>c__DisplayClass.CS$<>8__locals5 = this; 27 Console.WriteLine(this.str + "!"); 28 <>c__DisplayClass.name = "张三"; 29 this.<>4__this.ShowNested = new Action(<>c__DisplayClass.<Init>b__1); 30 this.<>4__this.SetNested = new Action<string>(<>c__DisplayClass.<Init>b__2); 31 } 32 public void <Init>b__3(string v) 33 { 34 this.str = v; 35 } 36 } 37 public Action Show; 38 public Action<string> Set; 39 public Action ShowNested; 40 public Action<string> SetNested; 41 public void Init() 42 { 43 A.<>c__DisplayClass4 <>c__DisplayClass = new A.<>c__DisplayClass4(); 44 <>c__DisplayClass.<>4__this = this; 45 <>c__DisplayClass.str = "你好"; 46 this.Show = (Action)Delegate.Combine(this.Show, new Action(<>c__DisplayClass.<Init>b__0)); 47 this.Set = (Action<string>)Delegate.Combine(this.Set, new Action<string>(<>c__DisplayClass.<Init>b__3)); 48 } 49 } 50 }
反编译出来的类A的定义与源代码中类A的定义已经不同,通过编译器的重构,以基本的C#语法实现了匿名函数和类似Javascript中闭包的功能。
3.结论
Javascript中所有函数本质上都是闭包,是在作用域的角度上对函数的称谓。
C#中的匿名函数行为特性上类似Javascript中闭包,通过编译器重构实现。
在Javascript中,函数是一个对象,因此函数中定义函数就是一件非常正常的事情。如果:
1 function foo() { 2 var bar = new Function("val", "return val"); 3 return bar("test"); 4 }; 5 foo();
看起来很正常,那么:
1 function foo() { 2 function bar(val) { 3 return val; 4 }; 5 return bar("test"); 6 }; 7 foo();
也是很正常的。