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

认识,c#,foreach,语法,真的,全部 · 浏览次数 : 2018

小编点评

**C# for循环陷阱** 1. **闭包捕获变量的时空和闭包执行的时空不是一个时空**:闭包捕获的是变量的时空,闭包执行的时空是每次循环所执行的代码的时空。 2. **foreach在循环迭代时,给我们搞出了局部变量**:`foreach` 语法会为每个元素创建一个新的局部变量,并将其绑定到循环中的变量。 3. **闭包和全局变量之间的关系**:闭包捕获的是全局变量,闭包执行时捕获的变量是全局变量的值。 4. **泛泛循环**:`for` 语法和 `foreach` 语法都是用于遍历列表的,但它们在执行效率上有所不同。泛型循环使用 `for` 语句,而 `foreach` 语句使用 `yield return` 语句。 5. **闭包与循环变量之间的区别**:闭包变量是函数的局部变量,循环变量是函数的全局变量。当闭包被执行时,其变量会覆盖全局变量的值。

正文

本文的知识点其实由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<Action>();
        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
    }
}

变量v的位置对于怎样捕获变量v是很重要的。

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null)
    {
        f = () => Console.WriteLine("First value: " + value);
    }
}
f();

If v in the expanded form were declared outside of the while loop, it would be shared among all iterations, and its value after the for loop would be the final value, 13, which is what the invocation of f would print. Instead, because each iteration has its own variable v, the one captured by f in the first iteration will continue to hold the value 7, which is what will be printed. (Note that earlier versions of C# declared v outside of the while loop.)

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

泛泛而言

循环迭代这两个术语到底该怎么理解? 在各大语言的实现机制是怎样的 ?

循环: 满足某种条件, 而重复执行一段代码, 上面的demo1输出5个5, 是因为重复执行的代码公用了同一个全局自由变量。

迭代: 按顺序去访问一个列表中的每一项,C# 早期迭代变量也是作为公共变量, 现在是作为循环的内部变量, 于是形成了与for循环的差异。


以上理解透彻之后,我们再看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循环变量是相对的全局变量, 也正是这个差异,导致了投票题的结果。

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

与你认识的C# foreach语法糖,真的是全部吗?相似的内容:

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

> 本文的知识点其实由golang知名的for循环陷阱发散而来, 对应到我的主力语言C#, 其实牵涉到闭包、foreach。为了便于理解,我重新组织了语言,以倒叙结构行文。 先给大家提炼出一个C#题:观察for、foreach闭包的差异 ![](https://files.mdnice.com/us

带你认识3个J.U.C组件扩展

摘要:本文主要为大家讲解3种J.U.C组件扩展。 本文分享自华为云社区《【高并发】J.U.C组件扩展》,作者: 冰 河。 1.FutureTask FutureTask是J.U.C(java.util.concurrent)下的,但不是AQS(AbstractQueuedSynchronizer)的

3天上手Ascend C编程丨带你认识Ascend C基本概念及常用接口

Ascend C是华为昇腾面向算子开发场景的编程语言,想省时省力快速入门可以看这篇文章,为你系统化梳理AscendC编程最重要的知识点,3天快速上手不迷路。

5大特性,带你认识化繁为简的华为云CodeArts Deploy

摘要:2月27日,华为云发布持续部署服务CodeArts Deploy,通过模块化自由编排部署流程,实现软件的自动化部署,帮助企业软件产品的快速、高效、高质量交付。 本文分享自华为云社区《化繁为简高效部署 华为云发布部署服务CodeArts Deploy》,作者:华为云头条。 随着互联网、数字化的发

带你认识数仓的监控系统TopSQL

TopSQL为DWS的监控系统,记录DWS中各个作业、算子级别的资源使用数据、耗时数据,包括下盘信息、内存、网络、耗时、警告、基础信息等作业执行的数据。

带你认识数仓的增量备份核心设计

摘要:增量备份是重要的常规备份策略,正确快速识别增量变化文件的相关信息对增量备份至关重要。 本文分享自华为云社区《其疾如风,GaussDB(DWS)增量备份核心设计》,作者: 我的橘子呢 。 1、认识增量备份 GaussDB(DWS)数仓的备份恢复工具Roach支持集群级增量备份。全量备份会将源数据

Python从0到1丨带你认识图像平滑的三种线性滤波

摘要:常用于消除噪声的图像平滑方法包括三种线性滤波(均值滤波、方框滤波、高斯滤波)和两种非线性滤波(中值滤波、双边滤波),本文将详细讲解三种线性滤波方法。 本文分享自华为云社区《[Python从零到壹] 五十五.图像增强及运算篇之图像平滑(均值滤波、方框滤波、高斯滤波)》,作者:eastmount。

认识CPU底层原理(1)——MOSFET

本文为B站UP主硬件茶谈制作的系列科普《【硬件科普】带你认识CPU》系列的学习笔记,仅作个人学习记录使用,如有侵权,请联系博主删除 近年来,由于国内外各种因素影响,半导体行业逐渐被推向风口浪尖,这时人们才认识到,作为早已宣称进入信息化和数字化时代的中国,在计算机基础设施建设上是如此薄弱。 计算机基础

认识 CPU 底层原理(2)——逻辑门

本文为B站UP主硬件茶谈制作的系列科普《【硬件科普】带你认识CPU》系列的学习笔记,仅作个人学习记录使用,如有侵权,请联系博主删除 上一篇文章我们从最基本的粒子的角度认识了组成CPU的最基本单元MOSFET的原理。CPU作为计算机的核心硬件,其最主要的功能是进行运算,本章我们将会介绍CPU设计者是如

带你认识JDK8中超nice的Native Memory Tracking

摘要:从 OpenJDK8 起有了一个很 nice 的虚拟机内部功能: Native Memory Tracking (NMT)。 本文分享自华为云社区《Native Memory Tracking 详解(1):基础介绍》,作者:毕昇小助手。 0.引言 我们经常会好奇,我启动了一个 JVM,他到底会