【问题标题】:Why does append modify passed slice为什么追加修改传递的切片
【发布时间】:2016-03-10 15:22:26
【问题描述】:

我如何遍历切片并将切片传递到除当前元素之外的某个位置?正如我们在文档中看到的那样,似乎 append() 函数修改了底层切片。但无论如何我仍然不知道如何达到这个目标。

func main() {
    args := []string{ "2", "3", "8" }

    for i, _ := range args {
        fmt.Println(append(args[:i], args[i+1:]...)) // or pass to function
    }

    fmt.Println(args)
}

结果:

[3 8]
[3 8]
[3 8]
[3 8 8] // it is args now

我的期望:

 [3 8]
 [2 8]
 [2 3]

我已经看到了这个Why does append() modify the provided slice? (See example)

但是切片的容量是什么对我来说是个秘密,我不明白为什么我会超过它。

【问题讨论】:

  • 如果不想修改切片,则需要将值复制到新切片。除此之外,我真的不明白你的问题。你能展示你期望发生的事情吗?
  • JimB,我修改了问题,以便您可以看到我的期望
  • 使用full-slice expression 作为append 的第一个参数就可以了:append(args[:i:i], args[i+1:]...)

标签: go


【解决方案1】:

性能是主要原因。创建一个新切片并将所有元素复制到其中是昂贵的,因此切片代码不会在没有充分理由的情况下复制。但是,如果超出了切片的容量,它会通过复制底层切片来适当地增长。这意味着从append 返回的切片可能与您传入的切片不同。

首选的使用方式是:

args = append(args, newarg)

如果您采用子切片,则容量保持不变,但您对切片的视图会发生变化。这意味着缺失的元素仍然存在,但在新切片的范围之外。

这解释了您的代码的奇怪输出。您每次都打印append 的结果,但不存储该结果,这意味着args 与您打印的不同。

最初的 args 切片是 3 个元素大。对于每个索引i - 也就是说对于012 - 你获取一个子切片args[:i] 并将数组args[i+1:] 其余部分的所有元素附加到它。这意味着:

i    args[:i]     args[i+1]...   Result         args
0    {}           {"3", "8"}     {"3", "8"}     {"3", "8", "8"}
1    {"3"}        {"8"}          {"3", "8"}     {"3", "8", "8"}
2    {"3", "8"}   {}             {"3", "8"}     {"3", "8", "8"}

tl;dr您应该始终保存append 的结果,如果您想复制一份以便可以更改它,请自己复制一份。

【讨论】:

    【解决方案2】:

    Append 总是尝试修改底层数组。

    让我们看看循环的第一次执行

    append(args[:0], args[0+1:]...)
    

    这样做是将切片 {3,8} 附加到切片 {},因为 args[:0] 为您提供了一个空切片,该切片在数组的开头结束。这就是为什么您的数组以 [3 8 8] 出现的原因,因为 3 8 被附加到数组中。 阅读更多关于此on the wiki 的信息。

    您可以使用 make 设置默认容量,即

    args := make([]string, 0, CAPACITY)
    

    您还可以检查切片的容量

    a := []int{1,2,3}
    fmt.Println(cap(a))
    >>> 3
    

    最后,如果您不想像 Elwinar 的回答那样每次都重新复制数组,我建议将两个切片 a[:i] 和 a[i+1:] 传递给函数。

    【讨论】:

      【解决方案3】:

      The Go Programming Language Specification

      Appending to and copying slices

      普通切片中的内置函数追加和复制辅助 操作。对于这两个函数,结果与是否 参数引用的内存重叠。

      可变参数函数 append 将零个或多个值 x 附加到 s 的 类型 S,必须是切片类型,并返回结果切片, 也是 S 类型的。值 x 被传递给 ...T 类型的参数 其中 T 是 S 的元素类型和相应的参数传递 规则适用。作为一种特殊情况,append 也接受第一个参数 可分配给类型 []byte 并带有字符串类型的第二个参数 后跟...。这种形式附加了字符串的字节。

      append(s S, x ...T) S  // T is the element type of S
      

      如果 s 的容量不足以容纳附加值, append 分配一个新的,足够大的底层数组,适合 现有的切片元素和附加值。除此以外, append 重用底层数组。

      s0 := []int{0, 0}
      s1 := append(s0, 2)                // append a single element     s1 == []int{0, 0, 2}
      s2 := append(s1, 3, 5, 7)          // append multiple elements    s2 == []int{0, 0, 2, 3, 5, 7}
      s3 := append(s2, s0...)            // append a slice              s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
      s4 := append(s3[3:6], s3[2:]...)   // append overlapping slice    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
      
      var t []interface{}
      t = append(t, 42, 3.1415, "foo")   //                             t == []interface{}{42, 3.1415, "foo"}
      
      var b []byte
      b = append(b, "bar"...)            // append string contents      b == []byte{'b', 'a', 'r' }
      

      函数 copy 将切片元素从源 src 复制到 目标 dst 并返回复制的元素数。两个都 参数必须具有相同的元素类型 T 并且必须可分配给 []T 类型的切片。复制的元素数量是最小的 len(src) 和 len(dst)。作为一种特殊情况,copy 也接受一个 目标参数可分配给类型 []byte 与源参数 字符串类型的。这种形式将字符串中的字节复制到 字节切片。

      copy(dst, src []T) int
      copy(dst []byte, src string) int
      

      例子:

      var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
      var s = make([]int, 6)
      var b = make([]byte, 5)
      n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
      n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
      n3 := copy(b, "Hello, World!")  // n3 == 5, b == []byte("Hello")
      

      不要使用 append 覆盖您的输入。为您的输出使用单独的变量(函数参数)。例如,

      package main
      
      import "fmt"
      
      func main() {
          args := []string{"2", "3", "8"}
          fmt.Println(args)
          funcArg := make([]string, len(args)-1)
          for i := range args {
              copy(funcArg, args[:i])
              copy(funcArg[i:], args[i+1:])
              fmt.Println(funcArg)
          }
          fmt.Println(args)
      }
      

      输出:

      [2 3 8]
      [3 8]
      [2 8]
      [2 3]
      [2 3 8]
      

      【讨论】:

        【解决方案4】:

        您可以将切片想象成一个结构,该结构由一个固定大小的数组和一个用于计算其中元素数量的计数器组成。切片的容量是底层数组的大小,切片的长度是计数器。

        Append 是这样定义的:func append(slice []Type, elems ...Type) []Type (godoc),这实质上意味着您将把 elem 可变参数附加到 slice 参数。如果len(elems) + len(slice) > cap(slice),则需要将底层数组更改为更大的数组(具有更大的容量),这意味着(在运行中)一个新切片(因此返回参数)。

        在您的情况下,您没有超出切片的容量。你刚刚修改了它的内容。

        一个简单(尽管有点难看)的技巧是将两个附加嵌套到一个空切片:

        package main
        
        import "fmt"
        
        func main() {
            args := []string{ "2", "3", "8" }
        
            for i, _ := range args {
                fmt.Println(append(append([]string{}, args[:i]...), args[i+1:]...)) 
            }
        
            fmt.Println(args)
        }
        

        或者,如果你想将切片的副本传递给一个方法(然后做你想做的事),你可以使用copy 函数……

        【讨论】:

          【解决方案5】:

          现有答案充分解释了 OP 观察到的行为并提供了可行的解决方案。然而,没有人提到在 Go 1.2(2012 年末)中引入的full slice expressions(又名three-index slices)。使用 one 作为 append 的第一个参数以简洁的方式解决了 OP 的问题。为了完整起见,我在此处包括了该方法:

          package main
          
          import "fmt"
          
          func main() {
              args := []string{"2", "3", "8"}
              for i, _ := range args {
                  fmt.Println(append(args[:i:i], args[i+1:]...))
              }
              fmt.Println(args)
          }
          

          (Playground)

          输出:

          [3 8]
          [2 8]
          [2 3]
          [2 3 8]
          

          从 Go 1.18 开始,您可以使用 golang.org/x/exp/slices package 中的函数 CloneDelete 以更易读的方式实现相同的结果:

          package main
          
          import (
              "fmt"
          
              "golang.org/x/exp/slices"
          )
          
          func main() {
              args := []string{"2", "3", "8"}
              for i, _ := range args {
                  fmt.Println(slices.Delete(slices.Clone(args), i, i+1))
              }
              fmt.Println(args)
          }
          

          (Playground)

          【讨论】:

            猜你喜欢
            • 2018-08-31
            • 1970-01-01
            • 2017-06-17
            • 1970-01-01
            • 2022-01-06
            • 1970-01-01
            • 2021-10-26
            • 2016-10-20
            • 2019-09-09
            相关资源
            最近更新 更多