【问题标题】:Slice within a loop seems to retain the previous/last reference (depending on the length of slice)循环中的切片似乎保留了前一个/最后一个引用(取决于切片的长度)
【发布时间】:2020-11-03 15:32:30
【问题描述】:

这看起来很奇怪,在循环中有一个局部变量slice,为每个循环分配了新值,我将该切片附加到全局sliceWrappers。循环完成后,全局切片内的所有值仅包含对该局部切片变量上设置的最后一个值的引用。

代码:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var sliceWrappers [][]string
    initialSlice := append([]string{}, "hi")
    initialSlice = append(initialSlice, "there")
    
    // Remove this line and it works fine
    initialSlice = append(initialSlice, "I'm")
    
    for i := 0; i < 2; i++ {
        slice := append(initialSlice, strconv.Itoa(i))
        fmt.Printf("Slice Value : %+v, Initial Value : %+v\n", slice, initialSlice)
        sliceWrappers = append(sliceWrappers, slice)
    }

    for _, sliceWrapper := range sliceWrappers {
        fmt.Printf("%+v\n", sliceWrapper)
    }
}

实际输出:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 1]
[hi there I'm 1]

预期输出:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 0]  <------ This is not happening
[hi there I'm 1]

如果我删除 initialSlice = append(initialSlice, "I'm") 行,那么它可以完美运行。

Slice Value : [hi there 0], Initial Value : [hi there]
Slice Value : [hi there 1], Initial Value : [hi there]
[hi there 0]  <------ Works Perfectly
[hi there 1]

我相信这与append有关

append 内置函数将元素追加到切片的末尾。 如果它有足够的容量,则将目标重新切片到 适应新元素。

如果上述条件是造成它的原因,那么循环内打印的initialSlice的值不应该也与slice相同吗?

游乐场 - https://play.golang.org/p/b3SDGoA2Lzv

PS:不幸的是,我为我的代码编写了只有 3 层嵌套的测试用例,它通过了。我现在必须为循环内的切片处理 copy

【问题讨论】:

  • @Flimzy 不,它不同。在那个问题中,它是关于在范围发生时修改原始切片。这是一个完全不同的循环,我只是尝试将一个值附加到切片并打印它。如果原始切片有 2 个值,则它可以工作,但如果其中有 3 个值,则它会失败。

标签: loops go reference slice


【解决方案1】:
// Remove this line and it works fine
//initialSlice = append(initialSlice, "I'm")
fmt.Printf("Slice Value : %p - %d\n", initialSlice, cap(initialSlice))
...
for i := 0; i < 2; i++ {
    slice := append(initialSlice, strconv.Itoa(i))
    fmt.Printf("Slice Value : %p - %p\n", slice, initialSlice)
    ...
}

如上打印initialSlicesliceaddresscapacity。 当取消注释追加我是行。输出如下:

Slice Value : 0xc00009c000 - 2
Slice Value : 0xc0000a6000 - 0xc00009c000
Slice Value : 0xc00009e040 - 0xc00009c000

如果注释该行,输出如下:

Slice Value : 0xc00009e000 - 4
Slice Value : 0xc00009e000 - 0xc00009e000
Slice Value : 0xc00009e000 - 0xc00009e000

然后为什么在注释行时它会按预期输出?
因为在这个场景中,initialSlice的容量是2。当追加新元素时,将分配一个新的底层数组,因为它没有足够的空间来实现追加操作。
而当你取消注释该行时,initialSlice的容量为4,它会就地修改数组。

参考:附加文档

append 内置函数将元素追加到切片的末尾。如果 它有足够的容量,目的地被重新分割以容纳 新元素。如果没有,将分配一个新的底层数组。 Append 返回更新后的切片。因此有必要存储 附加的结果,通常在保存切片本身的变量中: 切片 = 追加(切片,elem1,elem2) 切片 = 附加(切片,另一个切片...) 作为一种特殊情况,将字符串附加到字节切片是合法的,如下所示: slice = append([]byte("hello"), "world"...)

【讨论】:

  • +1 完美。这正是我所怀疑的。但有了这个解释,我有了更多的怀疑。如果重复使用相同的切片(就地修改),那么为什么 initialSlice 的值总是打印原始值?我的意思是sliceInitialSlice 的值在打印时不应该相同吗?
  • 基本上这段代码应该为两个变量打印相同的值,因为它们的地址是相同的。 play.golang.org/p/VbA2AIPAnFi
【解决方案2】:

切片基于指向数组的指针、长度和容量。您在第一次迭代时将slice 放入sliceWrappers(因此它包含指向数组的指针)。在第二次迭代中,append(initialSlice, strconv.Itoa(i)) 调用更改了同一数组中的值,因为内存位置没有改变。在第一次和第二次迭代中,slice 都指向了这个数组,因此以sliceWrappers 结尾的两个切片都指向相同的数据。

您可以通过在将数据添加到sliceWrappers 之前将数据复制到新切片来避免这种情况:

    for i := 0; i < 2; i++ {
        slice := append(initialSlice, strconv.Itoa(i))
        fmt.Printf("Slice Value : %+v, Initial Value : %+v\n", slice, initialSlice)
        copiedSlice := make([]string, len(slice))
        copy(copiedSlice, slice)
        sliceWrappers = append(sliceWrappers, copiedSlice)
    }

这给出了预期的输出:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 0]
[hi there I'm 1]

至于删除initialSlice = append(initialSlice, "I'm")这一行:当你追加到一个切片时,它会检查它是否可以在容量内适应新的长度。如果没有,它将分配一个新数组(从而分配新的内存位置)。包含“hi there”的较短切片已达到其容量,附加到它将分配一个新数组并创建一个容量更大的切片。

  • 如果您的程序中有initialSlice = append(initialSlice, "I'm") 行,则新数组将在循环之前分配。循环内的append(...)s 不会导致新的分配。
  • 如果您的程序中没有该行,循环内的append(...)s 将导致新的分配,因此每个最终都有不同的内存位置,这就是它们不会相互覆盖的原因。

我的来源是 Go Slices:用法和内部 https://blog.golang.org/slices-intro#TOC_4

【讨论】:

  • 我不认为输出正在做你所说的。如果是这种情况,那么为什么如果删除此行 initialSlice = append(initialSlice, "I'm") 代码会正常工作,则指针不会根据切片内的元素数量而运行。
  • 因为您尝试放入其中的字符串的长度超过了切片的容量。 append 然后将返回一个切片,该切片具有指向内存中不同位置的指针。我可能在第一句话中措辞过于简单,我将对其进行编辑以澄清。
  • 没错,这与我的想法和@nail 解释的相似。但我仍然不确定下面发生了什么。当它使用相同的数组时(因为它有剩余容量),在这种情况下,返回切片和原始切片的指针地址将相同。但是它显示的值是不同的,即使它们具有相同的地址。任何想法? play.golang.org/p/VbA2AIPAnFi哦,明白了。切片内部,包含一个指针,长度为字段。所以在显示它时使用长度字段来确定要打印多少个值,即使它们具有相同的指针引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-17
  • 1970-01-01
  • 2014-04-27
  • 1970-01-01
  • 2021-12-16
  • 2015-02-23
相关资源
最近更新 更多