【问题标题】:Assigning slice vs redeclaring slice in Go在 Go 中分配切片与重新声明切片
【发布时间】:2021-03-30 11:34:19
【问题描述】:

我正在尝试将切片用作队列数据结构,并且我想出了这个实现,这会导致无限循环。这是因为 queue 切片不会使用子切片 queue[1:] 进行更新。

func badQueue() {
    queue := []int{0,1,2,3,4,5}
    for len(queue) > 0 {
        current, queue := queue[0], queue[1:]
        fmt.Println(current, queue)
    }
}
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
...

我发现问题与我重新声明 currentqueue(使用 :=)而不是分配值有关,这解决了问题:

func goodQueue() {
    queue := []int{0,1,2,3,4,5}
    var current int
    for len(queue) > 0 {
        current, queue = queue[0], queue[1:]
        fmt.Println(current, queue)
    }
}
0 [1 2 3 4 5]
1 [2 3 4 5]
2 [3 4 5]
3 [4 5]
4 [5]
5 []

我知道导致问题的原因,但我不完全理解为什么在这种情况下重新声明操作与分配的工作方式不同。为什么队列没有用队列的子片(queue[1:])重新声明?

谢谢!

【问题讨论】:

  • “我不完全理解为什么在这种情况下重新声明操作的工作方式与分配不同。” -- 因为 Go 是 block scoped

标签: go slice assign declare


【解决方案1】:

因为您可以拥有多个具有相同名称的变量,只要它们具有不同的作用域即可。内部作用域中的变量将遮蔽外部作用域中的变量。

所以如果我们分解你的例子

func badQueue() {
    // queue from outer scope, lets call it A
    queue := []int{0,1,2,3,4,5}
    // the only visible queue here is A, so len(queue) will always refer to A
    for len(queue) > 0 {
        // same thing here, the only visible queue is A, so queue[0] and queue[1:]
        // both refer to A
        // We are also declaring new variables, queue and current
        // This queue is now shadowing the outer queue, let's call this one B
        current, queue := queue[0], queue[1:]

        // Here queue will refer to B
        fmt.Println(current, queue)

        // When the current iteration of the loop ends current and queue B is destroyed
        // because they go out of scope and the loop start over with A unchanged
    }
}

【讨论】:

  • 谢谢,现在很有意义!
【解决方案2】:

在简短声明的情况下,变量queue 仅适用于循环体,即为每次新的迭代销毁和创建。正如Short variable declarations中所引用的那样

与常规变量声明不同,短变量声明可以重新声明变量,前提是它们最初在同一个块(或参数列表,如果块是函数体)中具有相同类型,并且至少有一个非-空白变量是新的。因此,重新声明只能出现在多变量短声明中。重新声明不引入新变量;它只是为原始值分配一个新值。

【讨论】:

    【解决方案3】:

    理解这在 Go 中是如何工作的需要两点:

    1. 范围

      Go 使用block scope,每个大括号对将创建一个新作用域,如果它们具有相同的声明名称,则内部作用域中的标识符将覆盖外部作用域中的标识符。

       func main() {
           var name string = "Golang"
           fmt.Printf("Outer Scope: %s\n", name) // Print "Golang"
           {
               var name string = "Java"
               fmt.Printf("Inner Scope: %s\n", name)   // Print "Java"
           }   
           fmt.Printf("Outer Scope: %s\n", name)  // Print "Golang" again
        }
      
    2. Short variable declaration

      操作符:= 是一个复合操作,它会在一个语句中做几件事:声明、类型推断和赋值,基本上你可以把它当作语法糖。以下 S1 和 S2 的代码示例是等价的:

      func main() {
          // S1
          var name string = "Golang" 
      
          // S2
          name := "Golang"
      }
      

    考虑到以上两点,翻译后的代码如下:

    func badQueue() {
        queue := []int{0,1,2,3,4,5}
        for len(queue) > 0 {
            var current int
            var queue []int
            current, queue = queue[0], queue[1:]
            fmt.Println(current, queue)
        }
    }
    

    很明显,外部queuefor 循环内部没有受到影响。

    顺便说一句,对于:= 左侧的每个变量,编译器将查找当前块范围以尝试解析标识符,如果之前已经声明过,编译器将重用它而不是创建一个新的.但是如果之前声明了所有 lhs 变量,编译器将报告错误消息“no new variables on left side of :=”。查看以下代码:

    func main() {
        var name string
        name, age := "Golang", 10 // reuse the above 'name' and create a new variable 'age'
    
        var tom, jerry string
        tom, jerry := "Tom", "Jerry" // no new variables on left side of :=
    }
    

    new compiler implementation 在这里,给那些对细节感兴趣的人。

    【讨论】:

      猜你喜欢
      • 2016-07-22
      • 2014-10-22
      • 1970-01-01
      • 2019-12-22
      • 2016-05-12
      • 1970-01-01
      • 2012-11-10
      • 2023-03-09
      相关资源
      最近更新 更多