CLR 中匿名函數的實現原理淺析
C# 2.0中提供了通過delegate實現匿名函數功能,能有效地減少用戶的薄記代碼工作,例如
以下為引用:
... button1.Click += new EventHandler(button1_Click); ... void button1_Click(Object sender, EventArgs e) { // Do something, the button was clicked... } ...
可以被簡化為直接使用匿名函數構造,如
以下為引用:
... button1.Click += delegate(Object sender, EventArgs e) { // Do something, the button was clicked... } ...
關于匿名函數的使用方法可以參考Jeffrey Richter的Working with Delegates Made Easier with C# 2.0一文。簡要說來就是C#編譯器自動將匿名函數代碼轉移到一個自動命名函數中,將原來需要用戶手工完成的工作自動完成。例如構造一個私有靜態函數,如
以下為引用:
class AClass { static void CallbackWithoutNewingADelegateObject() { ThreadPool.QueueUserWorkItem(delegate(Object obj) { Console.WriteLine(obj); }, 5); } }
被編譯器自動轉換為
以下為引用:
class AClass { static void CallbackWithoutNewingADelegateObject() { ThreadPool.QueueUserWorkItem(new WaitCallback(__AnonymousMethod$00000002), 5); }
private static void __AnonymousMethod$00000002(Object obj) { Console.WriteLine(obj); } }
而這里自動生成的函數是否為static,編譯器根據使用此函數的地方是否static決定。這也是為什么C# 2.0規范里面禁止使用goto, break和continue語句從一個匿名方法里跳出,或從外面跳入其中的原因,因為他們代碼雖然寫在一個作用域里面,但實際上實現上并不在一起。 更方便的是編譯器可以根據匿名函數使用的情況,自動判斷函數參數,無需用戶在定義時指定,如
以下為引用:
button1.Click += delegate(Object sender, EventArgs e) { MessageBox.Show("The Button was clicked!"); };
在不使用參數時,完全等價于
以下為引用:
button1.Click += delegate { MessageBox.Show("The Button was clicked!"); };
相對于匿名函數的實現來說,比較復雜的是匿名函數對于其父作用域中變量的使用及其實現。MS的Grant Ri在其blog上有一系列的討論文章。 Anonymous Methods, Part 1 of ? Anonymous Methods, Part 2 of ? Anonymous Method Part 2 answers
需要解決的問題有兩個:一是不在一個變量作用域中的匿名函數如何訪問父函數和類的變量;二是匿名函數使用到的變量的生命周期必須與其綁定,而不能與父函數的調用生命周期綁定。這兩個問題使得C#編譯器選擇較為復雜的獨立類封裝方式實現匿名函數和相關變量生命周期的管理。
首先,匿名函數使用到的父函數中局部變量,無聊是引用類型還是值類型,都必須從棧變量轉換為堆變量,以便在其作用域外的匿名函數實現代碼可以訪問并控制生命周期。因為棧變量的生命周期與其所有者函數是一致的,所有者函數退出后,其堆棧自動恢復到調用函數前,也就無法完成變量生命周期與函數調用生命周期的解耦。 例如下面這個簡單的匿名函數中,使用了父函數的局部變量,雖然此匿名函數只在父函數里面使用,但C#編譯器還是使用獨立類對其使用到的變量進行了包裝。
以下為引用:
delegate void Delegate1();
public void Method1() { int i=0;
Delegate1 d1 = delegate() { i++; };
d1(); }
自動生成的包裝代碼類似如下
以下為引用:
delegate void Delegate1();
private sealed class __LocalsDisplayClass$00000002 { public int i;
public void __AnonymousMethod$00000001() { this.i++; } };
public void Method1() { __LocalsDisplayClass$00000002 local1 = new __LocalsDisplayClass$00000002(); local1.i = 0;
Delegate1 d1 = new Delegate1(local1.__AnonymousMethod$00000001);
d1(); }
但對于有多個局部變量作用域的情況就比較復雜了,例如Grant Ri在其例子中給出的代碼
以下為引用:
delegate void NoArgs();
void SomeMethod() { NoArgs [] methods = new NoArgs[10]; int outer = 0; for (int i = 0; i < 10; i++) { int inner = i; methods[i] = delegate { Console.WriteLine("outer = {0}", outer++); Console.WriteLine("i = {0}", i); Console.WriteLine("inner = {0}", ++inner); }; methods[i](); } for (int j = 0; j < methods.Length; j++) methods[j](); }
就需要一個類封裝變量outer;一個類封裝變量i;另外一個類封裝inner和匿名函數,并引用前面兩個封裝類的實例。因為變量outer、i和inner有著不同的作用域,呵呵。偽代碼如下:
以下為引用:
private sealed class __LocalsDisplayClass$00000008 { public int outer;
}; private sealed class __LocalsDisplayClass$0000000a { public int i;
}; private sealed class __LocalsDisplayClass$0000000c { public int inner;
public __LocalsDisplayClass$00000008 $locals$00000009; public __LocalsDisplayClass$0000000a $locals$0000000b;
public void __AnonymousMethod$00000007() { Console.WriteLine("outer = {0}", this.$locals$00000009.outer++); Console.WriteLine("i = {0}", this.$locals$0000000b.i); Console.WriteLine("inner = {0}", ++this.inner); } };
public void SomeMethod() { NoArgs [] methods = new NoArgs[10];
__LocalsDisplayClass$00000008 local1 = new __LocalsDisplayClass$00000008(); local1.outer = 0;
__LocalsDisplayClass$0000000a local2 = new __LocalsDisplayClass$0000000a(); local2.i = 0;
while(local2.i < 10) { __LocalsDisplayClass$0000000c local3 = new __LocalsDisplayClass$0000000c(); local3.$locals$00000009 = local1; local3.$locals$0000000b = local2; local3.inner = local1.i;
methods[local2.i] = new NoArgs(local3.__AnonymousMethod$00000007); methods[local2.i](); }
for (int j = 0; j < methods.Length; j++) methods[j](); }
總結其規律就是每個不同的局部變量作用域會有一個單獨的類進行封裝,子作用域中如果使用到父作用域的局部變量,則子作用域的封裝類引用父作用域的封裝類。相同作用域的變量和匿名方法由封裝類綁定到一起,維護其一致的生命周期。
相對于MS較為復雜的實現,Delphi.NET對嵌套函數則使用較為簡單的參數傳遞方式,因為嵌套函數沒有那么復雜的變量生命期管理要求,如
以下為引用:
procedure SayHello; var Name: string;
procedure Say; begin WriteLn(Name); end; begin Name := 'Flier Lu';
Say; end;
系統生成函數Say代碼時,將使用到的上級變量如Name放入到一個自動生成的類型($Unnamed1)中,然后作為函數參數傳遞給Say函數,偽代碼類似
以下為引用:
type $Unnamed1 = record Name: string; end;
procedure @1$SayHello$Say(var UnnamedParam: $Unnamed1); begin WriteLn(UnnamedParam.Name); end;
procedure SayHello; var Name: string; Unnamed1: $Unnamed1; begin Name := 'Flier Lu';
Unnamed1.Name := Name;
Say(Unnamed1); end;
|