问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C# foreach语法糖背后的秘密:闭包与迭代变量的那些事

创作时间:
作者:
@小白创作中心

C# foreach语法糖背后的秘密:闭包与迭代变量的那些事

引用
1
来源
1.
https://cloud.tencent.com/developer/article/2196843

本文通过对比C#中的for循环和foreach循环在闭包处理上的差异,深入探讨了为什么foreach能够正确输出迭代值,而for循环则不能。文章还进一步扩展到Go语言的for循环陷阱,并提供了相应的解决方案。

让我们先来看一个C#的示例代码:

var t1 = new List<Action>();
for (int i = 0; i < 5; i++)
{
    var func = (() =>
    {
        Console.WriteLine(i);
    });
    t1.Add(func);
}
foreach (var item in t1)
{
    item();
}

左边输出5个5;右边输出0,1,2,3,4。

闭包原理

闭包是在词法环境中捕获自由变量的头等函数。在上述代码中,关键在于闭包捕获的自由变量。

这里有三个关键概念需要理解:

  1. 局部变量i是被头等函数引用的自由变量
  2. 闭包捕获变量i的时空和闭包执行的时空不是一个时空
  3. 所有闭包执行时,捕获的都是变量i的最终值

解决方案

为了应对这种陷阱,可以在循环内使用一个局部变量来解构每个闭包与全局自由变量i的关系:

var t1 = new List<Action>();
for (int i = 0; i < 5; i++)
{
    var j = i;
    var func = (() =>
    {
        Console.WriteLine(j);
    });
    t1.Add(func);
}
foreach (var item in t1)
{
    item();
}

foreach的底层实现

foreach的底层实现依赖于IEnumerableIEnumerator两个接口。下面是foreach的伪代码:

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current; // 注意,变量v的定义是在循环体内
            /*embedded_statement*/
        }
    }
    finally
    {
        ... // Dispose e
    }
}

由于变量v的定义在while循环内部,因此使用foreach迭代时,每个闭包捕获的都是局部的自由变量,因此foreach闭包执行能输出0,1,2,3,4。

Golang的for循环陷阱

Golang中只有for循环,没有while和foreach关键字。下面是一个示例:

package main
import "fmt"
var slice []func()
func main() {
    sli := []int{1, 2, 3, 4, 5}
    for _, v := range sli {
        fmt.Println(&v, v)
        slice = append(slice, func() {
            fmt.Println(v) 
        })
    }
    for _, val := range slice {
        val()
    }
}

输出结果:

0xc00001c098 1
0xc00001c098 2
0xc00001c098 3
0xc00001c098 4
0xc00001c098 5
5
5
5
5
5

Golang的for-range循环本质上与C#的for循环相同,闭包引用的是全局的自由变量v。解决方法是在循环体内创建局部变量:

for _, v := range sli {
    v := v
    fmt.Println(&v, v)
    slice = append(slice, func() {
        fmt.Println(v) 
    })
}

总结

本文主要讨论了以下几个知识点:

  • 闭包:是在词法环境中捕获自由变量的头等函数
  • foreach语法糖:依赖于IEnumerable和IEnumerator接口实现,同时foreach每次迭代使用的是块内局部变量
  • for循环变量是相对的全局变量,这也是导致闭包陷阱的原因

这些知识点都很重要且较为晦涩,建议读者关注文中给出的参考资料,以便进一步学习和探讨。

参考资料

  1. C#闭包详解
  2. IEnumerator与IEnumerable的区别
  3. C# foreach语句官方文档
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号