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();

 

也是很正常的。

發佈留言