当前位置:编程学习 > asp >>

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

 

匿名方法

匿名方法是.NET 2.0中引入的高级特性,“匿名”二字说明它可以把实现内联地写在一个方法中,从而形成一个委托对象,而不用有明确地方法名,例如:

static void Test()

{

    Action<string> action = delegate(string value)

    {

        Console.WriteLine(value);

    };

 

    action("Hello World");

}

但是匿名方法的关键并不仅于“匿名”二字。其最强大的特性就在于匿名方法形成了一个闭包,它可以作为参数传递到另一个方法中去,但同时也能访问方法的局部变量和当前类中的其它成员。例如:

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

    }

}

如上所示,在TestClass的Test方法中,action委托调用了同在TestClass类中的私有方法Print,并对Test方法中的局部变量index进行了读写。在加上C# 3.0中Lambda表达式的新特性,匿名方法的使用得到了极大的推广。不过,如果使用不当,匿名方法也容易造成难以发现的问题。

问题案例

某位兄弟最近在一个简单的数据导入程序,主要工作是从文本文件中读取数据,进行分析和重组,然后写入数据库。其逻辑大致如下:

static void Process()

{

    List<Item> batchItems = new List<Item>();

    foreach (var item in ...)

    {

        batchItems.Add(item);

 

        if (batchItems.Count > 1000)

        {

            DataContext db = new DataContext();

            db.Items.InsertAllOnSubmit(batchItems);

            db.SubmitChanges();

 

            batchItems = new List<Item>();

        }

    }

}

每次从数据源中读取数据后,添加到batchItems列表中,当batchItems满1000条时便进行一次提交。这段代码功能运行正常,可惜时间卡在了数据库提交上。数据的获取和处理很快,但是提交一次就要花较长时间。于是想想,数据提交和数据处理不会有资源上的冲突,那么就把数据提交放在另外一个线程上进行处理吧!于是,使用ThreadPool来改写代码:

static void Process()

{

    List<Item> batchItems = new List<Item>();

    foreach (var item in ...)

    {

        batchItems.Add(item);

 

        if (batchItems.Count > 1000)

        {

            ThreadPool.QueueUserWorkItem((o) =>

            {

                DataContext db = new DataContext();

                db.Items.InsertAllOnSubmit(batchItems);

                db.SubmitChanges();

            });                   

 

            batchItems = new List<Item>();

        }

    }

}

现在,我们将数据提交操作交给ThreadPoll执行,当线程池中有额外线程时,就会发起数据提交操作。而数据提交操作不会阻塞数据处理,因此按照那位兄弟的意图,数据会不断进行处理,最后只要等待所有数据库提交完成就可以了。思路很好,可惜运行时发现,原本(不利用多线程时)运行正常的代码,如今会“莫名其妙”地抛出异常。更为奇怪的是,数据库中的数据出现了丢失的情况:处理了并“提交”了一百万条数据,但是数据库里却少了一部分。于是对着代码左看右看,百思不得其解。

您看出问题原因来了吗?

分析原因

要发现问题所在,我们必须了解匿名方法在.NET环境中的实现方式。

.NET中本没有什么“匿名方法”,也没有类似的新特性。“匿名方法”完全是由编译器施展的魔法,它会将匿名方法中需要访问的所有成员一起包含在闭包中,确保所有的成员调用都符合.NET标准。例如在文章第一节中的第2个示例,实际上由编译器处理之后就变成了如下的样子(自然字段名经过“友好化”处理):

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

    }

}

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

•     编译器自动生成一个私有的内部辅助类,并将其设为sealed,这个类的实例将成为一个闭包对象。

•     如果匿名方法需要访问方法的参数或局部变量,那么该参数或局部变量将“升级”成为辅助类中的公有Field字段。

•     如果匿名方法需要访问类中的其它方法,那么辅助类中将保存类的当

补充:Web开发 , ASP.Net ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,