using System;
var funs = new Action[10];
for (var i = 0; i < 10; i++)
funs[i] = () => Console.WriteLine(i);
foreach (var fn in funs)
fn();
猜测这段代码运行结果 1-9,实际运行结果为全部的 10 在 SharpLab 中查看
为什么呢,看反编译以后的代码是什么样子的
// 这里删除了一些不必要的代码引入和声明,为了减少篇幅
[CompilerGenerated]
internal class Program
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int i;
internal void <<Main>$>b__0()
{
Console.WriteLine(i);
}
}
private static void <Main>$(string[] args)
{
Action[] array = new Action[10];
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.i = 0;
while (<>c__DisplayClass0_.i < 10)
{
array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
<>c__DisplayClass0_.i++;
}
Action[] array2 = array;
int num = 0;
while (num < array2.Length)
{
Action action = array2[num];
action();
num++;
}
}
}
请看 <Main>$
函数中的代码,让我们看看他编译以后是如何对作用域引用的变量进行捕获的。
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.i = 0;
while (<>c__DisplayClass0_.i < 10)
{
array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
<>c__DisplayClass0_.i++;
}
实际主要就是 Main 函数中的这段代码,编译器创建了一个类 <>c__DisplayClass0_0
来捕获 Action 引用的变量,但是该类的实例是在循环外面,所以的 Action 都共享同样一个对象。所以解决这个问题十分简单,只需要让编译每次循环是都创建一个 <>c__DisplayClass0_0
类的实例对象就可以了。
有两种方法,我们先看第一种
// 这里只给出循环的代码
// loop
for (var i = 0; i < 10; i++)
{
var num = i; # 在循环内声明变量,明确指明作用范围,指示编译器
funs[i] = () => Console.WriteLine(num);
}
// end
然后我们看看反编译后循环类的代码
while (num < 10)
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.num = num;
array[num] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
num++;
}
ok,没问题
第二种方法
foreach (var i in Enumerable.Range(0, 10))
{
var num = i; // 在循环内声明变量,明确指明作用范围,指示编译器
funs[i] = () => Console.WriteLine(num);
}
// end
这里是借助编译器对 foreach 的处理,所以反编译后的代码有些不同,下面是完整的 Main 函数中的内容
// Main
Action[] array = new Action[10];
IEnumerator<int> enumerator = Enumerable.Range(0, 10).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); // 重要的还是在循环中创建
<>c__DisplayClass0_.i = enumerator.Current;
array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
Action[] array2 = array;
int num = 0;
while (num < array2.Length)
{
Action action = array2[num];
action();
num++;
}
// end Main
因为基本我日常的循环都被 foreach 和 LINQ 取代了,所以我从来没有发现过这个问题,直到我遇上了。我认为这个很违反直觉,我知道在其他语言中也有类似的行为,但是在 C# 中我认为他确实是违法直觉的,我不知道这算不算一个 bug。
拿 JS 和 Py 举例,他们两个(JS 在 es6 以前使用没有 let 和 const 的时代)根本就没有块级作用域的概念,只有函数作用域,所以这样的表现能够理解。而 C# 不是的,他是明确有块级作用域的概念,所以不知道在这里又突然表现不同。