本文的知识点其实由golang知名的for循环陷阱发散而来,
对应到我的主力语言C#, 其实牵涉到闭包、foreach。为了便于理解,我重新组织了语言,以倒叙结构行文。
先给大家提炼出一个C#题:观察for、foreach闭包的差异
左边输出 5个5; 右边输出0,1,2,3,4, 答对的可以不用看下文了。
闭包是在词法环境中捕获自由变量的头等函数, 题中关键是捕获的自由变量。
这里面有3个关键名词,希望大家重视,可以围观我之前的 👇新来的总监,把C#闭包讲得那叫一个透彻。
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();
}
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迭代一个很有意思的差异。
循环
、迭代
这两个术语到底该怎么理解? 在各大语言的实现机制是怎样的 ?
以上理解透彻之后,我们再看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 还能与多线程结合,又有不一样的现象。
本文其实内容很多:
每一个知识点都是比较重要且晦涩难懂,篇幅有限,请适时关注文中给出的几个永久更新地址。