一个感觉难以理解的的 C#代码片段,想知道这是为什么

2020-10-03 17:07:45 +08:00
 richards64

代码如下:

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

3390 次点击
所在节点    程序员
20 条回复
Soar360
2020-10-03 17:24:00 +08:00
闭包
across
2020-10-03 17:36:11 +08:00
我记得 c # via cia 好像解释过,这种函数的实质是创建一个类,引用 i 变量。因循环结束,引用的 i 值就变成 4 了...
mengqi
2020-10-03 17:47:42 +08:00
比较常见的因为惰性求值导致的问题。因为 Console.Write(...) 不是在 for (int i...) 循环内求值,而是延迟到了 foreach 里面求值,而在那时变量 i 值为 4,因此 foreach 打印了 4 遍 i 就是 "4 4 4 4" 了
crella
2020-10-03 17:52:20 +08:00
不懂 c#,感觉是 Console.Write()里并没有保存 i 的值,仅保存了对其的引用
crclz
2020-10-03 18:04:17 +08:00
闭包的知识点,详细的可以参考 js
YenvY
2020-10-03 18:08:42 +08:00
四个 lambda 捕获了同一个 i
richards64
2020-10-03 18:20:57 +08:00
@across 能大概说下在哪个章节吗?我翻翻看
lxilu
2020-10-03 18:23:00 +08:00
[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
hejingyuan199
2020-10-03 19:01:41 +08:00
这个我好像觉得很自然就是 4 4 4 4 。
但我不知道如何解释。
因为你在后面才调用的 foo,那时候 i 已经是 4 了。
那当然是打印出全是 4 啊。

我就排个队,看看各位如何解释。
lights
2020-10-03 19:15:15 +08:00
看具体的语言实现吧,这个是纯的硬性知识点了,只是刚好 C#里实现的是 8 楼所说的形式

不过好奇为啥是这种实现方式(四个 lambda 只 new 一个类)而不是 new 4 个类。有大佬解释一下吗?
lights
2020-10-03 19:16:35 +08:00
@across #2 有介绍这种设计的原因吗?四个 labmda 只 new 一个类,不应该是 new 4 个类吗?感觉挺反直觉的
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
zhuang0718
2020-10-03 19:53:34 +08:00
lambda 表达式的原因?
geelaw
2020-10-03 19:54:25 +08:00
@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 都会引用各自状态对象的方法。
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
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 了。
Youen
2020-10-03 21:48:03 +08:00
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} ");
}
laminux29
2020-10-04 23:39:25 +08:00
请问题主是遇到啥需求,写了这样一段奇怪的代码?
xuanbg
2020-10-05 03:16:00 +08:00
因为真正执行 Console.Write($"{i} ");不是在 for 循环里面,而是在 foreach 迭代里面呀。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/712372

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX