代码改变世界

浅析匿名方法中的局部变量

2011-07-18 14:30  PeterWang2004  阅读(461)  评论(0编辑  收藏  举报

  在上一篇文章中,我讨论了如何使用表达式树来柯里化一个函数。鹤老师提出了一个很简单的解决方法,那就是使用闭包。的确,经过我的测试,无论在性能或者是表现方式上都超越了使用表达式树,这是因为在表达式树组装过程中使用了反射。感谢鹤老师在这里给出的答案,但是细想一下,还是发现使用闭包来做柯里化函数(使用C# 4.0),可能会存在一些风险,这主要是涉及到闭包中局部变量的问题。

  我相信大学习Linq的时候,一定听过“什么叫做延长局部变量生命周期”的言语。今天我主要就是要说明下这个问题,但要澄清一点,我绝对没有反驳鹤老师的意思。只是为大家分享一些开发中可能出现的问题,多一种选择,则多一份收获。

  那么,首先来看看一份答案吧:

 Func<int, int,int> tempCurry = Accumulation;
Func
<int, int> curryed = i => tempCurry(i, 100);

 curryed就是我们柯里化后的函数,编译、运行,一起呵成。完全看不出任何问题。呵呵,本来其实就没有问题(似乎有点废话了),但我把它放在下面的这个应用中又会如何呢?这个函数的目的是把baseSum(累加基数)做为一个变量,然后柯里化10个函数,最后返回到主调函数中去。你能从中看出潜在的风险么?

 static Func<int,int>[] SomethingAboutQuesion()
{
var curryedFuncs
= new Func<int, int>[10];
Func
<int,int,int> curryTarget=Accumulation;
for (int baseSum = 1; baseSum <= 10; baseSum++)
{
curryedFuncs[baseSum
- 1] = count => curryTarget(baseSum, count);
}
return curryedFuncs;

}

  嗯,对了,风险是局部变量baseSum所带来的,因为看似像curryedFuncs中每一个函数都引用了不同的baseSum值,但实际上他们引用的是相同的值,这是为什么呢?我们知道,当在匿名方法中访问当前作用域外的变量时(这种行为称为闭包),编译器会做一些处理,处理的方式可以简单描述如下:与闭包中使用过的且相同作用域的变量(非静态变量,对于静态变量,请自己尝试)以及闭包本身会生成一个类,并且这个类会引用它上一级作用域生成的类(如果有的话),最后,下一级作用域的类会引用上一级作用域生成的类作为一个字段,这样一级一级的递归下去。在上面这段代码中编译器就会为我们自动的生成2个类,其中一个类A包括了for作用域里面的变量baseSum以及闭包本身,另一个类B包括了SomethingAboutQuesion中的curryTarget。并且A中会保存一个B的字段。因此,当修改baseSum的值,其实就是相当于变相修改类A中的baseSum字段,这就是为什么导致风险的原因了。运行返回的所有函数,就会得到下面的这个结果:

 SomethingAboutQuesion().ToList().ForEach(s =>
{
Console.WriteLine(s(
100));
});
//5061
//5061
//5061
//....
//5061

 那么如何解决这个问题呢?最简单的解决方法是,我们只需要在离闭包最近的那个作用域中提供一个包裹类,把所有闭包中访问的外部作用域的变量放到包裹类当中,然后让闭包依赖这一个类就行了:

var curryedFuncs = new Func<int, int>[10];
Func
<int,int,int> curryTarget=Accumulation;
for (int baseSum = 1; baseSum <= 10; baseSum++)
{
var wapper
= new { curryTargerWrapper = curryTarget, baseSumWapper=baseSum };
curryedFuncs[baseSum
- 1] = count => wapper.curryTargerWrapper(wapper.baseSumWapper, count);
}
return curryedFuncs;

  请再运行测试代码,即可以得到正确的结果了。但请注意,千万不要在以后对wapper进行赋值(比如wapper=null),值得一提的是,如果重新设置curryTarget的值会如何呢?请尝试一下。

  其实,对于上面这一个问题,在Erlang中是不存在的,因为Erlang这样的函数式编程语言中完全是靠变量复制(copy)而来的。在Erlang中我们可以这样实现Curry:

-module(acc).
-export([curry/2]).


accumulation(Sum,Count) when Count
=/=0->
accumulation(Sum
+Count,Count-1);
accumulation(Sum,
0)->
Sum.

curry(Result,Sum)
->
case Sum=<10 of
true->
curry([fun(Count)
->accumulation(Sum,Count) end|Result],Sum+1);
false->
Result
end.

  然后在shell中运行,即可得到下面的结果:

[(fun(CurryedFun)->CurryedFun(100) end)(X)||X<-acc:curry([],1)].
result: [
5060,5059,5058,5057,5056,5055,5054,5053,5052,5051]

  请一定注意匿名方法中访问局部变量的问题,弄清楚到底是复制,还是引用。这样才不会在开发过程中遇到这样稀奇古怪的问题。曾经我有一位朋友就是在,这个问题上纠结了二天。