代码改变世界

C#中匿名方法变量共享原理分析

2011-10-18 17:24  AnyKoro  阅读(410)  评论(0编辑  收藏  举报

先上几篇我分析中用到例子的文章

利用Reflector把“闭包”看清楚

警惕匿名方法造成的变量共享

 

进入正题,正向老赵说的那样,c#本来没有什么匿名方法之说,只是编译器施展的魔法。那么我们首先先看看老赵文中提出的,魔法施展的方法。

“编译器将匿名方法中需要访问的所有成员一起包含在闭包中,确保所有的成员调用都符合.NET标准。”

它的处理方式可以用老赵“友好化”处理的类来说明。

源代码:

class TestClass
{
    private void Print(string message)
    {
        Console.WriteLine(message);
    }

    public void Test()
    {
        string[] messages = new string[] { "Hello", "World" };
        int index = 0;

        Action<string> action = (m) =>
        {
            this.Print((index++) + ". " + m);
        };

        Array.ForEach(messages, action);
        Console.WriteLine("index = " + index);
    }
}

编译器处理后的代码: 

class TestClass
{
    ...

    private sealed class AutoGeneratedHelperClass
    {
        public TestClass m_testClassInstance;
        public int m_index;

        public void Action(string m)
        {
            this.m_index++;
            this.m_testClassInstance.Print(m);
        }
    }

    public void TestAfterCompiled()
    {
        AutoGeneratedHelperClass helper = new AutoGeneratedHelperClass();
        helper.m_testClassInstance = this;
        helper.m_index = 0;

        string[] messages = new string[] { "Hello", "World" };
        Action<string> action = new Action<string>(helper.Action);
        Array.ForEach(messages, action);

        Console.WriteLine(helper.m_index);
    }
}

 

由此就可以看出编译器是如何实现一个闭包的:

  1. 编译器自动生成一个私有的内部辅助类,并将其设为sealed,这个类的实例将成为一个闭包对象。
  2. 如果匿名方法需要访问方法的参数或局部变量,那么该参数或局部变量将“升级”成为辅助类中的公有Field字段。
  3. 如果匿名方法需要访问类中的其它方法,那么辅助类中将保存类的当前实例。

这三点感觉说得不够详细,我在这里做更多的解释

AutoGeneratedHelperClass是形成匿名方法的关键。根据匿名方法的特点,函数块中使用到了局部变量。这句话可能比较难懂,我们可以看这段代码

     class Program
    {
        
static void Main(string[] args)
        {
            List
<Action> ls = new List<Action>();
            
for (int i = 0; i < 10; i++)
            {                
                ls.Add(() 
=> Console.WriteLine(i));                
            }

            
foreach (Action action in ls)
            {
                action();
            }
            System.Console.Read();
        }       
    } 

 

在这里你可以发现,在ls.Add(() => Console.WriteLine(i));这句代码中,使用到了局部变量i。(注意是在函数体内使用到局部变量,而不是作为参数传入。)

 有了这个基础后,我们再看之前的,在TestAfterCompiled()中是如何调用这个功能的。我们发现在主要用到了AutoGeneratedHelperClass类,在该方法中,实例化了一个AutoGeneratedHelperClass类,并且通过给该实例对象的字段赋值的手段,来使得可以正常的运行功能。所以,简单理解,在.net中匿名方法的实现,其实就是把匿名方法用到的局部变量封装在一个类(称为N类)中,同时该类还有一个通匿名方法具有相同实现的方法。以此解决了局部变量声明周期的问题。同样的对于匿名方法调用类(使用匿名方法的类,简称O类)方法,也就是在N类中加入一个O类的实例。

<>c__DisplayClass2类就是我们说的N类,然后把注意力放到高亮部分,这个class2就是对应老赵的helper类。由于i赋值的阶段在for之前,所以该类的生成就在循环之前。这样以来就要出问题了。尽管你Add进去的item的形式是对的,但是其所用到得N类却是同一个(类是引用类型),当执行完循环后class2还是class2只是其中的i变了。既然10个action指向同一个N类,那么他们必然打印出10个相同的结果。