Go 中的捕获闭包(循环变量)

问题描述 投票:0回答:2

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

for-loop go closures
2个回答
19
投票

2,2

输出:

变量循环 3 3 3 价值循环 0 1 2 可变范围 2 2 2 取值范围 0 1 2

参考资料:

Go 编程语言规范

函数文字

函数文字是闭包:它们可能引用定义在 一个周围的函数。然后这些变量在 围绕函数和函数文字,它们作为 只要它们可以访问。

Go 常见问题解答:闭包作为 goroutine 运行会发生什么?

要在启动时将 v 的当前值绑定到每个闭包,一 必须修改内部循环以在每次迭代中创建一个新变量。 一种方法是将变量作为参数传递给闭包。

更简单的是使用声明创建一个新变量 这种风格可能看起来很奇怪,但在 Go 中运行良好。


1
投票
wiki 页面记录了您的过程(将循环变量复制到新变量中)

但是提案 # 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
语句可以是短变量声明(

init

),但post语句不能是。
每次迭代都有自己单独声明的变量(或多个变量)。


第一次迭代使用的变量由

:=
    语句声明。
  • 每个后续迭代使用的变量在执行 post 语句之前隐式声明,并初始化为当时前一个迭代变量的值。
  • init
自从 
Go 1.21 发布

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

输出(带注释):
// 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


© www.soinside.com 2019 - 2024. All rights reserved.