V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
richards64
V2EX  ›  程序员

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

  •  
  •   richards64 · 2020-10-03 17:07:45 +08:00 · 3417 次点击
    这是一个创建于 1513 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码如下:

    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

    第 1 条附言  ·  2020-10-03 18:37:11 +08:00

    刚刚发现发上来的时候把变量名打错了,for循环里的funcs[i]应该是foos[i]

    20 条回复    2020-10-05 03:16:00 +08:00
    Soar360
        1
    Soar360  
       2020-10-03 17:24:00 +08:00
    闭包
    across
        2
    across  
       2020-10-03 17:36:11 +08:00 via Android
    我记得 c # via cia 好像解释过,这种函数的实质是创建一个类,引用 i 变量。因循环结束,引用的 i 值就变成 4 了...
    mengqi
        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" 了
    crella
        4
    crella  
       2020-10-03 17:52:20 +08:00
    不懂 c#,感觉是 Console.Write()里并没有保存 i 的值,仅保存了对其的引用
    crclz
        5
    crclz  
       2020-10-03 18:04:17 +08:00
    闭包的知识点,详细的可以参考 js
    YenvY
        6
    YenvY  
       2020-10-03 18:08:42 +08:00
    四个 lambda 捕获了同一个 i
    richards64
        7
    richards64  
    OP
       2020-10-03 18:20:57 +08:00
    @across 能大概说下在哪个章节吗?我翻翻看
    lxilu
        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
    hejingyuan199
        9
    hejingyuan199  
       2020-10-03 19:01:41 +08:00
    这个我好像觉得很自然就是 4 4 4 4 。
    但我不知道如何解释。
    因为你在后面才调用的 foo,那时候 i 已经是 4 了。
    那当然是打印出全是 4 啊。

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

    不过好奇为啥是这种实现方式(四个 lambda 只 new 一个类)而不是 new 4 个类。有大佬解释一下吗?
    lights
        11
    lights  
       2020-10-03 19:16:35 +08:00
    @across #2 有介绍这种设计的原因吗?四个 labmda 只 new 一个类,不应该是 new 4 个类吗?感觉挺反直觉的
    youla
        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
    zhuang0718
        13
    zhuang0718  
       2020-10-03 19:53:34 +08:00
    lambda 表达式的原因?
    geelaw
        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 都会引用各自状态对象的方法。
    lxilu
        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
    viWww0vvxmolvY5p
        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 了。
    Youen
        17
    Youen  
       2020-10-03 21:48:03 +08:00
    dartabe
        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} ");
    }
    laminux29
        19
    laminux29  
       2020-10-04 23:39:25 +08:00
    请问题主是遇到啥需求,写了这样一段奇怪的代码?
    xuanbg
        20
    xuanbg  
       2020-10-05 03:16:00 +08:00
    因为真正执行 Console.Write($"{i} ");不是在 for 循环里面,而是在 foreach 迭代里面呀。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   950 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 22:42 · PVG 06:42 · LAX 14:42 · JFK 17:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.