CLR 中匿名函數的實現原理淺析

http://flier_lu.blogone.net/?id=1397624

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;


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章