你认识的C# foreach语法糖,真的是全部吗?
本文的知识点其实由golang知名的for循环陷阱发散而来,
对应到我的主力语言C#, 其实牵涉到闭包、foreach。为了便于理解,我重新组织了语言,以倒叙结构行文。
先给大家提炼出一个C#题:观察for、foreach闭包的差异
左边输出 5个5; 右边输出0,1,2,3,4, 答对的可以不用看下文了。
闭包是在词法环境中捕获变量的函数, 捕获的是变量的引用。
这里面有3个关键名词,希望大家重视,可以围观我之前的 👇新来的总监,把C#闭包讲得那叫一个透彻。
C# for (int i=0) 变量i相对于for循环代码块是全局变量
- for循环内产生了多个闭包函数,相对于每个闭包函数,i是全局变量, 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(); }
C# foreach(var i in items) i 在循环内是局部变量
foreach内闭包,为什么能输出预期的0,1,2,3,4。
聪明的读者可以猜想,是不是foreach在循环迭代时 ,给我们搞出了局部变量j,帮我们解构了闭包与全局自由变量i多对1的关系。
foreach的底层实现有赖于IEnumerable
和IEnumerator
两个接口的实现、
这里也有一个永久更新的原创文,👇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迭代一个很有意思的差异。
所以有关此类问题的关键点在于“
1> 循环变量相对于循环结构是全局变量还是局部变量
2> 函数闭包始终捕获的是 变量的引用。
golang for-range的演化
golang 除了经典的三段式for循环, 还有帮助快速遍历 map slice array chnanel的 for-range循环。
golang 1.21之前的for-range 循环变量也是全局变量(每次循环共享), 一直被开发者诟病, 在1.21之后for-range内的循环变量变成了 局部变量。
不管怎么演进,闭包函数捕获的内容始终没有变化,还是变量的引用。
1.21版本之前
package main import "fmt" var slice []func() func main() { sli := []int{1, 2, 3, 4, 5} for _, v := range sli { fmt.Println(&v, v) // 此处无闭包,for-range正常打印 slice = append(slice, func() { fmt.Println(v) // 此处形成闭包, 捕获了全局变量v的引用 }) } for _, val := range slice { val() } } --- output --- 0xc00001c098 1 0xc00001c098 2 0xc00001c098 3 0xc00001c098 4 0xc00001c098 5 5 5 5 5 5
循环变量相对全局, 每个闭包引用的都是(相对全局的)变量v,最终闭包执行的是同一个变量。
应对这种陷阱的思路,依旧是使用循环内局部变量去解构闭包与相对全局变量v的关系。
golang里面可以在loop body内v:=v
产生局部变量,覆盖全局的v。
另外 闭包 foreach 还能与多线程结合,又有不一样的现象。
画外音
本文其实内容很多:
- 闭包:是在词法环境中捕获自由变量的函数
- foreach 语法糖:依赖于IEnumerable和IEnumerator 接口实现,同时 foreach每次迭代使用的是块内局部变量, for循环变量是相对的全局变量, 也正是这个差异,导致了投票题的结果。
每一个知识点都是比较重要且晦涩难懂,篇幅有限,请适时关注文中给出的几个永久更新地址。
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/16907679.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
2019-11-21 EFCore批量操作内幕