• 你认识的C# foreach语法糖,真的是全部吗?


    本文的知识点其实由golang知名的for循环陷阱发散而来,
    对应到我的主力语言C#, 其实牵涉到闭包、foreach。为了便于理解,我重新组织了语言,以倒叙结构行文。

    先给大家提炼出一个C#题:观察for、foreach闭包的差异

    左边输出 5个5; 右边输出0,1,2,3,4, 答对的可以不用看下文了。


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

    这里面有3个关键名词,希望大家重视,可以围观我之前的 👇新来的总监,把C#闭包讲得那叫一个透彻

    demo1

    • for循环内闭包,局部变量i是被头等函数引用的自由变量;相对于每个头等函数,i是全局变量;
    • 闭包捕获变量i的时空和 闭包执行的时空不是一个时空;
    • 所有闭包执行时,捕获的都是变量i,所以执行输出的都是i++最后的5。

    这也是C#闭包的陷阱, 通常应对方式是循环内使用一个局部变量解构每个闭包与(相对全局)变量i的关系。

     var t1 = new List();
            for (int i = 0; i < 5; i++)
            {
             // 使用局部变量解绑闭包与全局自由变量i的关系,现在自由变量是局部变量j了。
                var j = i;
                var func = (() =>
                {
                    Console.WriteLine(j);
                });
                t1.Add(func);
            }
            foreach (var item in t1)
            {
                item();
            }
    

    demo2

    foreach内闭包,为什么能输出预期的0,1,2,3,4。

    聪明的读者可以猜想,是不是foreach在循环迭代时 ,给我们搞出了局部变量j,帮我们解构了闭包与全局自由变量i多对1的关系。

    foreach的底层实现有赖于IEnumerableIEnumerator两个接口的实现、

    这里也有一个永久更新的原创文,👇IEnumerator、IEnumerable还傻傻分不清楚?

    但是怎么用这个两个接口,还需要看foreach伪代码:
    C# foreach foreach (V v in x) «embedded_statement»被翻译成下面代码。

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

    👇 foreach官方信源

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

    如果变量V v定义在while语言上方,那么效果就和for循环一样了。

    这是for循环/foreach迭代一个很有意思的差异。


    以上理解透彻之后,我们再看Golang的for循环陷阱, 也就很容易理解了。

    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()
    	}
    }
    --- output ---
    0xc00001c098 1
    0xc00001c098 2
    0xc00001c098 3
    0xc00001c098 4
    0xc00001c098 5
    5
    5
    5
    5
    5
    

    golang 除了经典的三段式for循环, 还有帮助快速遍历 map slice array chnanel的 for-range循环。
    两者的内核 都是C# for循环。

    循环变量相对全局, 每个闭包引用的都是(相对全局的)自由变量v,最终闭包执行的是同一个变量。
    应对这种陷阱的思路,依旧是使用循环内局部变量去解构闭包与相对全局变量v的关系。

    golang里面可以在loop body内v:=v产生局部变量,覆盖全局的v。

    另外 闭包 foreach 还能与多线程结合,又有不一样的现象。

    画外音

    本文其实内容很多:

    • 闭包:是在词法环境中捕获自由变量的头等函数
    • foreach 语法糖:依赖于IEnumerable和IEnumerator 接口实现,同时 foreach每次迭代使用的是块内局部变量, for循环变量是相对的全局变量, 也正是这个差异,导致了投票题的结果。

    每一个知识点都是比较重要且晦涩难懂,篇幅有限,请适时关注文中给出的几个永久更新地址。

  • 相关阅读:
    复习单片机:中断系统(内含1.中断概念+2 中断结构及相关寄存器)(注:相关寄存器是重点)
    大学生《Web课程谁》期末网页制作 HTML+CSS+JavaScript 网页设计实例 瑜伽网站企业网站制作
    金九银十进大厂必刷的Java面试题 (全彩版)
    LVS-DR模式
    jenkins-2.426.1-1.1.noarch.rpm 的公钥没有安装
    Spring注入Bean的几种方法
    天拓四方分享:智能装备制造厂商如何利用远程运维提升用户设备价值
    基于JAVA校园表白墙服务平台计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    学Python的漫画漫步进阶 -- 第十步
    特斯拉质量总监离职,竟是被马斯克挚友挖走?
  • 原文地址:https://www.cnblogs.com/JulianHuang/p/16907679.html