C#警惕匿名方法造成的变量共享实例分析

本文实例讲述了C#警惕匿名方法造成的变量共享。分享给大家供大家参考,具体如下:

匿名方法

匿名方法是.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字段。

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

值得一提的是,在实际情况下以上三点理论都皆可能不满足。在某些特别简单的情况下(例如匿名方法中完全不涉及局部变量和其他方法),编译器只会简单生成一个静态的方法来构造一个委托实例,因为这样可以获得更好的性能。

对于之前的案例,我们现在也将它进行一番改写,这样便可“避免”使用匿名对象,也可以清楚地展现出问题原因:

private class AutoGeneratedClass
{
  public List<Item> m_batchItems;
  public void WaitCallback(object o)
  {
    DataContext db = new DataContext();
    db.Items.InsertAllOnSubmit(this.m_batchItems);
    db.SubmitChanges();
  }
}
static void Process()
{
  var helper = new AutoGeneratedClass();
  helper.m_batchItems = new List<Item>();
  foreach (var item in ...)
  {
    helper.m_batchItems.Add(item);
    if (helper.m_batchItems.Count > 1000)
    {
      ThreadPool.QueueUserWorkItem(helper.WaitCallback);
      helper.m_batchItems = new List<Item>();
    }
  }
}

编译器会自动生成一个AutoGeneratedClass类,并且在Process方法中使用这个类的实例来代替原来的batchItems局部变量。同样,交给ThreadPool的委托对象也从匿名方法变成了AutoGeneratedClass实例的公有方法。因此线程池每次调用的便是该实例的WaitCallback方法。

现在问题应该一目了然了吧?每次把委托交给线程池之后,线程池并不会立即执行,而会保留到合适的时间再进行。而WaitCallback方法在执行时,它会读取m_batchItems这个Field字段“当前”所引用的对象。而与此同时,Process方法已经“抛弃”了原本我们要提交的数据,因此会引起提交到数据库中数据的丢失。同时,在准备每批次数据的过程中,很有可能会发起两次数据提交,两个线程提交同样一批Item时,就抛出了所谓“莫名其妙”的异常。

解决问题

找到了问题所在,解决起来自然轻而易举:

private class WrapperClass
{
  private List<Item> m_items;
  public WrapperClass(List<Item> items)
  {
    this.m_items = items;
  }
  public void WaitCallback(object o)
  {
    DataContext db = new DataContext();
    db.Items.InsertAllOnSubmit(this.m_items);
    db.SubmitChanges();
  }
}
static void Process()
{
  List<Item> batchItems = new List<Item>();
  foreach (var item in ...)
  {
    batchItems.Add(item);
    if (batchItems.Count > 1000)
    {
      ThreadPool.QueueUserWorkItem(
        new WrapperClass(batchItems).WaitCallback);
      batchItems = new List<Item>();
    }
  }
}

这里我们明确地准备一个封装类,用它来保留我们需要提交的数据。而每次提交时则使用保留好的数据,自然不会发生不该有的“数据共享”,从而避免了错误的发生1。

总结

匿名方法是强大的,但是也会造成一些令人难以察觉的陷阱。对于使用匿名方法创建的委托,如果不会立即同步执行,并且其中使用了方法的局部变量,那么您就需要对其留个心眼了。因为此时“局部变量”事实上已经由编译器转变成一个自动类的实例上的Field字段,而这个字段将被当前方法和委托对象共享。如果您在创建了委托对象之后还会修改共享的“局部变量”,那么请再三确认这样做符合您的意图,而不会造成问题。

此类问题也不光会出现在匿名方法中。如果您使用Lambda表达式创建了一个表达式树,其中也用到了一个“局部变量”,那么表达式树在解析或执行时同样也会获取“当前”的值,而不是创建表达式树时的值。

这也是为什么Java中的内联写法——匿名类——如果要共享方法内的“局部变量”,则必须将变量使用final关键字来修饰:这样这个变量只能在声明时赋值,避免了后续的“修改”可能会造成的“古怪问题”。

希望本文所述对大家C#程序设计有所帮助。

(0)

相关推荐

  • 初步认识C#中的Lambda表达式和匿名方法

    写在前面 元旦三天在家闲着无事,就看了看Linq的相关内容,也准备系统的学习一下,作为学习Linq的前奏,还是先得说说Lambda与匿名方法的知识点.也算是对知识点的查漏补缺吧,也许你会说这没啥大不了的,项目中都在用,但是有些知识,你回头在查看的时候,总会有那么点不一样的收获,这点我是感同身受的,我看书有个习惯,一本书,我能看个三四遍,每次总会有收获.当然,你可以说,当时肯定没认真看,不是那样子的,我认为最直接的原因在于,当时你看是看,没有在真正的项目中遇到过,所以你心里对它的理解并不深,如果在

  • C# 匿名方法基础回顾

    目录 简介 匿名方法的参数使用范围 委托示例 简介 在 C# 2.0 之前的版本中,我们创建委托的唯一形式 -- 命名方法. 而 C# 2.0 -- 引进了匿名方法,在 ≥ C# 3.0 的版本中,我们会用 Lambda 表达式进行取代匿名方法,并且用 Lambda 表达式作为编写内联代码的首选方式,因为它更简洁. 匿名方法是,顾名思义,匿名方法就是没有名称的方法.匿名方法最明显的好处就是:可以降低额外另写一个方法的工作量:另外一个好处就是可以直接访问调用者的变量,从而降低传参的复杂度. 匿名方

  • C#基础之匿名方法实例教程

    本文以实例形式讲解了C#的匿名方法的用法,分享给大家供大家参考之用.具体如下: 匿名方法是C# 2.0的语言新特性.首先看个最简单的例子: class Program { static void Main(string[] args) { List<string> names = new List<string>(); names.Add("Sunny Chen"); names.Add("Kitty Wang"); names.Add(&q

  • C#匿名方法与Delegate类型转换错误分析

    本文实例分析了C#匿名方法与Delegate类型转换错误.分享给大家供大家参考.具体分析如下: 问题描述 C#2.0出现了匿名方法, 这在一定程度上节省了我们维护代码上下文的精力, 也不需要思考为某个方法取什么名字比较合适. 在FCL的一些方法中要求传入一个Delegate类型的参数, 比如Control.Invoke或者Control.BeginInvoke方法: 复制代码 代码如下: public object Invoke(Delegate method); public IAsyncRe

  • C#实现在匿名方法中捕获外部变量的方法

    先来一段代码引入主题.如果你可以直接说出代码的输出结果,说明本文不适合你.(代码引自<深入理解C#>第三版) class Program { private delegate void TestDelegate(); static void Main(string[] args) { TestDelegate[] delegates = new TestDelegate[2]; int outside = 0; for(int i = 0; i < 2; i++) { int insid

  • C#特性之匿名方法和Lambda表达式

    在我们程序中,经常有这样一些需求: 1.       需要一个临时方法,这个方法只会使用一次,或者使用的很少. 2.       这个方法的方法体很短,以至于比方法声明都短,写起来实在没劲(我将其称之为"一句话方法"). 没办法,这样的方法写起来真是吃力不讨好,比如一些按钮事件处理中,有些按钮点击就是弹出一个对话框,或者调用一下别的什么方法.比如下面的代码: 复制代码 代码如下: this.btnRefresh.Click += new System.EventHandler(this

  • C#中的匿名方法实例解析

    本文较为详细的讲述了C#中的匿名方法,并附上实例加以说明.现将其分享给大家供大家参考之用.具体分析如下: 首先,C#中的匿名方法是在C#2.0引入的,它终结了C#2.0之前版本声明委托的唯一方法是使用命名方法的时代.虽然在 C# 3.0 及更高版本中,Lambda 表达式取代了匿名方法,作为编写内联代码的首选方式.但是,匿名方法的信息同样也适用于 Lambda 表达式,可以说 Lambda 表达式就是匿名方法演变过来的. 我们可以使用匿名方法来忽略参数列表. 这意味着匿名方法可转换为具有各种签名

  • c# 匿名方法的小例子

    复制代码 代码如下: // Create a handler for a click event.button1.Click += delegate(System.Object o, System.EventArgs e)                   { System.Windows.Forms.MessageBox.Show("Click!"); }; // Create a delegate.delegate void Del(int x); // Instantiate

  • C#用匿名方法定义委托的实现方法

    本文实例讲述了C#用匿名方法定义委托的实现方法.分享给大家供大家参考.具体实现方法如下: //用匿名方法定义委托 class Program { delegate string MyDelagate(string val); static void Main(string[] args) { string str1 = " 匿名方法外部 "; //中括号部分定义来了一个方法,没有名称,编译器会定指定一个名称 MyDelagate my = delegate(string param)

  • C#警惕匿名方法造成的变量共享实例分析

    本文实例讲述了C#警惕匿名方法造成的变量共享.分享给大家供大家参考,具体如下: 匿名方法 匿名方法是.NET 2.0中引入的高级特性,"匿名"二字说明它可以把实现内联地写在一个方法中,从而形成一个委托对象,而不用有明确地方法名,例如: static void Test() { Action<string> action = delegate(string value) { Console.WriteLine(value); }; action("Hello Wor

  • javascript变量声明实例分析

    本文实例讲述了javascript变量声明的方法.分享给大家供大家参考.具体分析如下: js中使用一个变量之前应当先声明.变量使用关键字var来声明. 如果未在var声明语句中给变量指定初始值,则该变量值为undefined. 不用在声明变量时指定变量类型,js变量可以是任意数据类型. 使用var语句重复声明变量是合法且无害的.如果重复声明带有初始化器,则就和简单的赋值语句没啥区别. 如果试图读取一个没有声明的变量,则js会报错.在ECMAScript5严格模式下,给一个没有声明的变量赋值也会报

  • mysql变量用法实例分析【系统变量、用户变量】

    本文实例讲述了mysql变量用法.分享给大家供大家参考,具体如下: 本文内容: 系统变量 用户变量 局部变量 首发日期:2018-04-18 系统变量: 系统变量就是系统已经提前定义好了的变量 系统变量一般都有其特殊意义.比如某些变量代表字符集.某些变量代表某些mysql文件位置 系统变量中包括会话级变量(当次会话连接生效的变量,如names),以及全局变量(一直生效的变量) [系统变量中全局变量和会话变量其实是使用一套变量,不同的是会话变量仅当次会话生效.] 会话变量的赋值:set 变量名 =

  • Python变量作用范围实例分析

    本文实例讲述了Python变量作用范围.分享给大家供大家参考.具体如下: #coding=utf-8 #变量作用范围 global z #使用全局变量 z=1 #给全局变量赋值 x=99 #x全局变量声明时初始化 def foo(y): #y和z在函数中被赋值:局部的 #局部区域 z=x+y #x没被赋值,所以它是全局的 return z def bar(y): global z z=x+y return z print foo(1) #结果=100 print z #结果=1 print ba

  • Java方法的可变参数类型实例分析

    Java方法中的可变参数类型是一个非常重要的概念,有着非常广泛的应用.本文就以实例形式对此加以分析.具体如下: 一般来说,许多Java初学者在看到下面的这段代码的时候,都会问一个问题:dealArray方法里那三个小点点是什么啊? public class TestVarArgus { public static void dealArray(int... intArray){ } public static void main(String args[]){ } } 这就是本文所要讨论的问题:

  • 微信小程序获取用户信息的两种方法wx.getUserInfo与open-data实例分析

    本文实例讲述了微信小程序获取用户信息的两种方法wx.getUserInfo与open-data.分享给大家供大家参考,具体如下: 在此之前,小程序获取微信的头像,昵称之类的用户信息,我用的都是wx.getUserInfo,例如: onLoad: function (options) { var that = this; //获取用户信息 wx.getUserInfo({ success: function (res) { console.log(res); that.data.userInfo

  • Java Lambda表达式的方法引用和构造器引用实例分析

    本文实例讲述了Java Lambda表达式的方法引用和构造器引用.分享给大家供大家参考,具体如下: 一 点睛 如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用,以使得Lambda表达式更加简洁. 种类 示例 说明 对应的Lambda表达式 引用类方法 类名::类方法 函数式接口中被实现方法的全部参数传给该类方法作为参数. (a,b,...) -> 类名.类方法(a,b, ...) 引用特定对象的实例方法 特定对象::实例方法 函数式接口中被实现方法的全部参数传

  • JS数组方法shift()、unshift()用法实例分析

    本文实例讲述了JS数组方法shift().unshift()用法.分享给大家供大家参考,具体如下: shift()方法 1. 定义:从数组中删除第一个元素,并返回该元素的值.此方法更改数组的长度. 2. 语法: arr.shift() 3. 参数:/ 4. 返回值:从数组中删除的元素(当数组为空时返回undefined). 代码如下: 方法一: var arr1 = [1, 2, 3, 4, 'A', 'B', 'C']; var arr2 = []; Array.prototype.copyS

  • JS数组方法push()、pop()用法实例分析

    本文实例讲述了JS数组方法push().pop()用法.分享给大家供大家参考,具体如下: push()方法 1. 定义:向数组的末尾添加一个或更多元素,并返回新的长度. 2. 语法: arr.push(element1, ..., elementN) 3. 参数:可以接收任意个数量的参数 4. 返回值:返回修改后数组的长度. var arr1 = [1, 2, 3, 4]; var arr2 = ["C", "B", "A"]; Array.pr

  • javascript执行上下文、变量对象实例分析

    本文实例讲述了javascript执行上下文.变量对象.分享给大家供大家参考,具体如下: 突然看到一篇远在2010年的老文,作者以章节的形式向我们介绍了ECMA-262-3的部分内容,主要涉及到执行上下文.变量对象.作用域.this等语言细节.内容短小而精悍,文风直白而严谨,读完有酣畅淋漓.醍醐灌顶之感,强烈推荐!!! 原文链接:这里 本想翻译成文,原来早已有人做了,这里.真生不逢时,何其遗憾啊! 做个笔记,聊慰我心. 执行上下文 ExecutionContext 每当控制器(control)转

随机推荐