警惕匿名方法造成的变量共享
解决问题
找到了问题所在,解决起来自然轻而易举:
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关键字来修饰:这样这个变量只能在声明时赋值,避免了后续的“修改”可能会造成的“古怪问题”。
注1:一个更简洁的解决方案可以参考29楼overred兄弟的回复。