Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

CLR 中匿名函数的实现原理浅析

Posted on 2004-07-08 10:31  Flier Lu  阅读(886)  评论(1编辑  收藏  举报
CLR 中匿名函数的实现原理浅析

http://www.blogcn.com/user8/flier_lu/index.html?id=1397624&run=.04B5CE2

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;