代码如下:
Action[] foos = new Action[4];
for (int i = 0; i < 4; i++)
{
funcs[i] = () => Console.Write($"{i} ");
}
foreach (var foo in foos)
{
foo();
}
输出如下:
4 4 4 4
为什么输出不是0 1 2 3
而是4 4 4 4
呢
刚刚发现发上来的时候把变量名打错了,for循环里的funcs[i]
应该是foos[i]
1
Soar360 2020-10-03 17:24:00 +08:00
闭包
|
2
across 2020-10-03 17:36:11 +08:00 via Android
我记得 c # via cia 好像解释过,这种函数的实质是创建一个类,引用 i 变量。因循环结束,引用的 i 值就变成 4 了...
|
3
mengqi 2020-10-03 17:47:42 +08:00
比较常见的因为惰性求值导致的问题。因为 Console.Write(...) 不是在 for (int i...) 循环内求值,而是延迟到了 foreach 里面求值,而在那时变量 i 值为 4,因此 foreach 打印了 4 遍 i 就是 "4 4 4 4" 了
|
4
crella 2020-10-03 17:52:20 +08:00
不懂 c#,感觉是 Console.Write()里并没有保存 i 的值,仅保存了对其的引用
|
5
crclz 2020-10-03 18:04:17 +08:00
闭包的知识点,详细的可以参考 js
|
6
YenvY 2020-10-03 18:08:42 +08:00
四个 lambda 捕获了同一个 i
|
7
richards64 OP @across 能大概说下在哪个章节吗?我翻翻看
|
8
lxilu 2020-10-03 18:23:00 +08:00 1
[CompilerGenerated]
private sealed class a_0 { public int i; internal void <M>b__0() { Console.Write(string.Format("{0} ", i)); } } public void M() { Action[] array = new Action[4]; a_0 a_ = new a_0(); a_.i = 0; while (a_.i < 4) { array[a_.i] = new Action(a_.<M>b__0); a_.i++; } Action[] array2 = array; for (int i = 0; i < array2.Length; i++) { array2[i](); } } 注:为便于观看有改名;编译器实现,准确见标准;来自 sharplab.io ; |
9
hejingyuan199 2020-10-03 19:01:41 +08:00
这个我好像觉得很自然就是 4 4 4 4 。
但我不知道如何解释。 因为你在后面才调用的 foo,那时候 i 已经是 4 了。 那当然是打印出全是 4 啊。 我就排个队,看看各位如何解释。 |
10
lights 2020-10-03 19:15:15 +08:00
看具体的语言实现吧,这个是纯的硬性知识点了,只是刚好 C#里实现的是 8 楼所说的形式
不过好奇为啥是这种实现方式(四个 lambda 只 new 一个类)而不是 new 4 个类。有大佬解释一下吗? |
11
lights 2020-10-03 19:16:35 +08:00
@across #2 有介绍这种设计的原因吗?四个 labmda 只 new 一个类,不应该是 new 4 个类吗?感觉挺反直觉的
|
12
youla 2020-10-03 19:41:51 +08:00
Action[] foos = new Action[4];
for (int i = 0; i < 4; i++) { foos[i] = () => Console.Write(i); foos[i].Invoke(); } //0 1 2 3 |
13
zhuang0718 2020-10-03 19:53:34 +08:00
lambda 表达式的原因?
|
14
geelaw 2020-10-03 19:54:25 +08:00 1
@lights #11 显然这里一共 new 了 5 个东西,一个是提供状态的对象(即编译器生成的),接下来每次赋值给委托的时候都 new 了一个 Action 。每个 Action 都引用了同一个状态对象的同一个方法,至于为什么同一个状态对象,这是因为 i 的 scope 只进入了一次。这里的 for 循环等价于
for (init; cond; next) body; { // 1 init; while (cond) { // 2 body; goto_continue: next; } // 2 } // 1 循环变量 i 的 scope 是 1,而 scope 1 只进入了一次,所以只有一个状态对象。 如果你改成 foreach (int i in new int[] { 0,1,2,3 }) 则在较新的 C# 编译器下会得到 0 1 2 3,因为这等价于 { // 1 var coll = new int[] { 0,1,2,3 }; for (int idx = 0; idx < coll.Length; ++idx) { // 2 int i = coll[idx]; foos[i] = () => Console.WriteLine(i); } // 2 } // 1 因为 i 的 scope 变成了 for 的里面,所以会进入 4 次,因此有 4 个不同的状态对象被创建,每个 Action 都会引用各自状态对象的方法。 |
15
lxilu 2020-10-03 20:08:02 +08:00
ECMA-334
5th Edition/December 2017 C# Language Specification 12. Expressions 12.16 Anonymous function expressions 12.16.6 Outer variables If a for-loop declares an iteration variable, that variable itself is considered to be declared outside of the loop. => i 在 for 外 Any local variable, value parameter, or parameter array whose scope includes the lambda-expression or anonymous-method-expression is called an outer variable of the anonymous function. => i 也在匿名函数外 A local variable is considered to be instantiated when execution enters the scope of the variable. => i 仅一实例 When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function. => 匿名函数用了那唯一 i 标准里的例子: ░░static D[] F() { ░░░░D[] result = new D[3]; ░░░░for (int i = 0; i < 3; i++) { ░░░░░░result[i] = () => { Console.WriteLine(i); }; ░░░░} ░░░░return result; ░░} only one instance of the iteration variable is captured, which produces the output: ░░3 ░░3 ░░3 |
16
viWww0vvxmolvY5p 2020-10-03 21:17:56 +08:00
跟变量的作用域有关系,i 是整个 for 循环体的局部变量,相当于给匿名委托引用了同一个函数,可以先将 i 复制给单次循环内的临时变量:
int n= i; foos[i] = () => Console.Write($"{n} "); 或者将 Lambda 转为有参的,foos[i] = n => Console.Write($"{n} ");调用时再把 i 传入。 还可以用多播委托, 这样打印出来的就是 0123 了。 |
17
Youen 2020-10-03 21:48:03 +08:00
|
18
dartabe 2020-10-04 04:57:32 +08:00
从 javascript 来看 变量作用域的原因 所有的 i 变量都是顶层作用域的 i
等价于 Action[] foos = new Action[4]; int I; for (i = 0; i < 4; i++) { funcs[i] = () => Console.Write($"{i} "); } |
19
laminux29 2020-10-04 23:39:25 +08:00
请问题主是遇到啥需求,写了这样一段奇怪的代码?
|
20
xuanbg 2020-10-05 03:16:00 +08:00
因为真正执行 Console.Write($"{i} ");不是在 for 循环里面,而是在 foreach 迭代里面呀。
|