前言
在公众号上看到一篇文章《正确使用和理解C#中的闭包》,里面提到了闭包的一个坑:
当捕获的外部变量为for循环的迭代变量时,C#认为变量i是定义在循环体外的。所以,当添加委托集合的for循环执行完时,i的值已经变为3了;因此,我们在foreach中循环调用委托时,i的值就都是3了。
List<Action> levyActions = new List<Action>();
for (int i = 0; i < 3; i++)
{levyActions.Add(()=> i.Dump());
}
foreach (Action action in levyActions)
{action();
}
那么,明明是循环体内定义的变量i,为什么会被认为定义在循环体外呢?
编译器魔法——Lowering
我们知道,C#代码最终会编译成IL中间语言。
假设有一个数组:
int[] arr = new[] { 0, 1, 2 };
我们可以有多种方式遍历它:
//1
foreach (var i1 in arr)
{i1.Dump();
}
//2
for (var i2 = 0; i2 < arr.Length; i2++)
{var value = arr[i2];value.Dump();
}
//3
var i3 = 0;
while(i3< arr.Length)
{var value = arr[i3];value.Dump();i3++;
}
那么,是不是要对应准备3种IL语法呢?
其实不是,在编译之前编译器还会施展一个魔法:Lowering
大概意思是,让编译器从高级语言功能“降低”到同一语言中的低级语言功能。
怎么理解这句话呢?让我们打开https://sharplab.io/
Roslyn编译器实现
sharplab.io这个网站可以显示.NET代码(比如c#)的编译中间过程和结果。
我们将上面的C#代码复制到窗口左边:
可以看到,编译器会将foreach和for语法都转换成while语法,这样,编译器最后只需要实现一种IL语法即可。
除了迭代以外,在roslyn编译器中实现了很多的“Lowering”,比如:
异步重写器
Lambda重写器
状态机重写器
详细列表你可以查看“https://github.com/dotnet/roslyn/tree/main/src/Compilers/CSharp/Portable/Lowering”下的代码。
结论
现在,大家应该已经知道,for循环中的变量i实际会被转换成while循环外定义的变量num,因此i在循环体作用域外也是有效的,导致了闭包的这个坑。
知道了原理,解决方案也很简单,始终使用循环体内的变量即可:
for (var i = 0; i < 3; i++)
{var j = i;levyActions.Add(() => j.Dump());
}
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“