Go 编译器不应该捕获
for...range
循环变量作为本地分配的闭包变量吗?
长版:
这也给我带来了一些困惑 在 C# 中,我试图理解它;这就是为什么它在 C# 5.0
foreach
中被修复(原因:循环变量 can not 在循环体内改变)以及不在 C# for
循环中修复它的原因(原因:循环变量 can在循环体内进行更改)。
现在(对我来说)Go 中的
for...range
循环看起来非常像 C# 中的 foreach
循环,但尽管我们无法更改这些变量(如 k
中的 v
和 for k, v := range m { ... }
);我们仍然必须首先将它们复制到一些本地闭包,以使它们按预期运行。
这背后的原因是什么? (我怀疑这是因为 Go 以同样的方式对待任何
for
循环;但我不确定)。
这里有一些代码来检查所描述的行为:
func main() {
lab1() // captured closure is not what is expected
fmt.Println(" ")
lab2() // captured closure is not what is expected
fmt.Println(" ")
lab3() // captured closure behaves ok
fmt.Println(" ")
}
func lab3() {
m := make(map[int32]int32)
var i int32
for i = 1; i <= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
kLocal, vLocal := k, v // (C) captures just the right values assigned to k and v
l = append(l, func() (int32, int32) {
return kLocal, vLocal
})
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}
func lab2() {
m := make(map[int32]int32)
var i int32
for i = 1; i <= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
l = append(l, func() (int32, int32) {
kLocal, vLocal := k, v // (B) captures just the last values assigned to k and v from the range
return kLocal, vLocal
})
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}
func lab1() {
m := make(map[int32]int32)
var i int32
for i = 1; i <= 10; i++ {
m[i] = i
}
l := [](func() (int32, int32)){}
for k, v := range m {
l = append(l, func() (int32, int32) { return k, v }) // (A) captures just the last values assigned to k and v from the range
}
for _, x := range l {
k, v := x()
fmt.Println(k, v)
}
}
如
lab1
所示,在注释 // (A)
处,我们仅从 range
中获取最后一个值;输出就像打印 9,9
十次,而不是显示预期结果,如 1,1
、2,2
...(当然,Go 中的地图不一定是排序的,所以我们可能会看到 3,3
十次)最后一对值;而不是最后一对值的十倍)。注释 10,10
和 // (B)
处的代码也是如此,这是预料之中的,因为我们试图捕获内部作用域内的外部变量(我也放了这个只是为了尝试一下)。在 lab2
注释处的代码中 lab3
一切正常,您将看到十对数字,例如 // (C)
,1,1
,....我尝试使用 closure+function
来替代 Go 中的 tuple。
但是提案 # 60078
package main
import "fmt"
func VariableLoop() {
f := make([]func(), 3)
for i := 0; i < 3; i++ {
// closure over variable i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("VariableLoop")
for _, f := range f {
f()
}
}
func ValueLoop() {
f := make([]func(), 3)
for i := 0; i < 3; i++ {
i := i
// closure over value of i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("ValueLoop")
for _, f := range f {
f()
}
}
func VariableRange() {
f := make([]func(), 3)
for i := range f {
// closure over variable i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("VariableRange")
for _, f := range f {
f()
}
}
func ValueRange() {
f := make([]func(), 3)
for i := range f {
i := i
// closure over value of i
f[i] = func() {
fmt.Println(i)
}
}
fmt.Println("ValueRange")
for _, f := range f {
f()
}
}
func main() {
VariableLoop()
ValueLoop()
VariableRange()
ValueRange()
}
:更不容易出错的循环变量作用域,2023 年 5 月,寻求解决这个问题。
更改循环语义本质上会为每个使用spec
声明的 for 循环变量插入这种:
v := v
语句。它将修复这个循环和许多其他循环,以实现作者明确的意图。新的循环语义仅适用于选择使用新循环发布的 Go 模块。如果那是 Go 1.22,那么只有带有:=
的模块中的包表示 go 1.22 才会获得新的循环语义。您可以在这里看到正在考虑的设计
该提案是关于更改 for 循环变量作用域语义,以便循环变量是按迭代而不是按循环。
对于带有
go.mod
子句的陈述将包括:
for
语句可以是短变量声明(Go 1.21 发布
init
),但post语句不能是。每次迭代都有自己单独声明的变量(或多个变量)。
第一次迭代使用的变量由:=
语句声明。
自从 每个后续迭代使用的变量在执行 post 语句之前隐式声明,并初始化为当时前一个迭代变量的值。init
Russ Cox 添加了 对于任何刚接触此问题并探索更改意味着什么的人来说,Go Playground 现在可以让您尝试新的语义。
为此,请使用 Go 1.21 并在程序顶部添加输出(带注释):。
var prints []func() for i := 0; i < 3; i++ { prints = append(prints, func() { println(i) }) } for _, p := range prints { p() } // Output: // 0 // 1 // 2
例如尝试https://go.dev/play/p/lDFLrPOcdz3,然后尝试删除评论。
// GOEXPERIMENT=loopvar
package main
func main() {
var prints []func()
for i := range make([]int, 5) {
prints = append(prints, func() { println(i) })
}
for _, p := range prints {
p()
}
}
输出(无注释):
0
1
2
3
4