为什么 append 修改了传递的切片?

huangapple go评论77阅读模式
英文:

Why does append modify passed slice

问题

我应该如何遍历切片并在某处传递除当前元素外的切片?似乎**append()**函数会修改底层切片,正如我们在文档中所看到的。但无论如何,我仍然不知道如何实现这一点。

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

    for i, _ := range args {
        fmt.Println(append(args[:i], args[i+1:]...)) // 或者传递给函数
    }

    fmt.Println(args)
}

结果:

[3 8]
[3 8]
[3 8]
[3 8 8] // 现在是args
我期望的是:
[3 8]
[2 8]
[2 3]
我已经看到了这个链接 https://stackoverflow.com/questions/17395261/why-does-append-modify-the-provided-slice-see-example
但是对我来说,切片的**容量**是个谜,我不明白为什么我超出了它。
<details>
<summary>英文:</summary>
How could I iterate through the slice and pass somewhere the slice except the current element? Seems **append()** function modifies the underlying slice as we could see in documentation. But anyway I still don&#39;t know how to reach this.
func main() {
args := []string{ &quot;2&quot;, &quot;3&quot;, &quot;8&quot; }
for i, _ := range args {
fmt.Println(append(args[:i], args[i+1:]...)) // or pass to function
}
fmt.Println(args)
}
result:
[3 8]
[3 8]
[3 8]
[3 8 8] // it is args now
what I expect:
[3 8]
[2 8]
[2 3]
I already saw this https://stackoverflow.com/questions/17395261/why-does-append-modify-the-provided-slice-see-example
but what is the **Capacity** of the slice is the secret for me, and I dont understand why did I exceed it.
</details>
# 答案1
**得分**: 12
性能是一个重要的原因。创建一个新的切片并将所有元素复制到其中是昂贵的,所以切片的代码不会没有充分的理由进行复制。然而,如果超过了切片的容量,它会通过复制底层切片来增长适当的数量。这意味着从`append`返回的切片可能不是你传入的同一个切片。
推荐的用法是:
args = append(args, newarg)
如果你取一个子切片,容量保持不变,但是你对切片的视图发生了改变。这意味着缺失的元素仍然存在,但超出了新切片的边界。
这解释了你的代码的奇怪输出。你每次打印`append`的结果,但没有存储那个结果,这意味着`args`和你打印的结果不一样。
初始的`args`切片有3个元素。对于每个索引`i` - 也就是说对于`0`、`1`和`2` - 你取一个子切片`args[:i]`,并将数组的剩余元素`args[i+1:]`的所有元素追加到其中。这意味着对于:
i    args[:i]     args[i+1:]...   结果         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"}
**简而言之**,你应该始终保存`append`的结果,如果你想要复制它以便进行更改,那么自己进行复制。
<details>
<summary>英文:</summary>
Performance is the big reason. Creating a new slice and copying all the elements over to it is expensive, so the slice code doesn&#39;t copy without good reason. However, if you exceed the slice&#39;s capacity, it grows by a suitable amount by copying the underlying slice. That means that the slice that&#39;s returned from `append` may not be the same slice you passed in.
The preferred way to use is:
args = append(args, newarg)
If you take a subslice, the capacity stays the same but your view into the slice changes. That means the missing elements are still there but outside the bounds of the new slice.
This explains the odd output of your code. You&#39;re printing the result of `append` each time but not storing that result, which means `args` isn&#39;t the same as what you printed.
The initial `args` slice is 3 elements big. For each index `i` - which is to say for `0`, `1` and `2` - you take a subslice `args[:i]` and append all the elements of the remainder of the array `args[i+1:]` to it. That means that for:
i    args[:i]     args[i+1:]...   Result         args
0    {}           {&quot;3&quot;, &quot;8&quot;}     {&quot;3&quot;, &quot;8&quot;}     {&quot;3&quot;, &quot;8&quot;, &quot;8&quot;}
1    {&quot;3&quot;}        {&quot;8&quot;}          {&quot;3&quot;, &quot;8&quot;}     {&quot;3&quot;, &quot;8&quot;, &quot;8&quot;}
2    {&quot;3&quot;, &quot;8&quot;}   {}             {&quot;3&quot;, &quot;8&quot;}     {&quot;3&quot;, &quot;8&quot;, &quot;8&quot;}
**tl;dr**  you should always save the result of `append`, and if you want to make a copy so you can change it then make a copy yourself.
</details>
# 答案2
**得分**: 6
Append函数总是尝试修改底层数组。
让我们看一下循环的第一次执行
append(args[:0], args[0+1:]...)
这样做的目的是将切片{3,8}追加到切片{}中,因为args[:0]给出了一个以数组开头为空的切片。这就是为什么你的数组变成了[3 8 8],因为3和8被追加到了数组中。
在[维基百科][1]上了解更多信息。
你可以使用make函数设置默认容量,例如:
args := make([]string, 0, CAPACITY)
你也可以检查切片的容量
a := []int{1,2,3}
fmt.Println(cap(a))
>>> 3
最后,如果你不想每次都复制数组,就像Elwinar的答案中所述,我建议将两个切片a[:i]和a[i+1:]传递给函数。
[1]: http://blog.golang.org/go-slices-usage-and-internals
<details>
<summary>英文:</summary>
Append always tries to modify the underlying array.
Lets look at the first execution of the loop
append(args[:0], args[0+1:]...)
What this does is append the slice {3,8} to the slice {} since args[:0] gives you an empty slice which ends at the start of the array. Thats why your array comes out as [3 8 8]because 3 8 gets appended to the array. 
Read more about this [on the wiki][1].
You can set a default capacity using make i.e.
args := make([]string, 0, CAPACITY)
You can also check the capacity for a slice 
a := []int{1,2,3}
fmt.Println(cap(a))
&gt;&gt;&gt; 3
Finally if you don&#39;t want to recopy the array every time as in Elwinar&#39;s answer I would recommend passing in the two slices, a[:i] and a[i+1:], to the function.
[1]: http://blog.golang.org/go-slices-usage-and-internals
</details>
# 答案3
**得分**: 3
《Go编程语言规范》是关于Go语言规范的官方文档。其中介绍了两个内置函数:append和copy,用于处理切片的常见操作。无论参数引用的内存是否重叠,这两个函数的结果都是独立的。
append是一个可变参数函数,它将零个或多个值x追加到类型为S的切片s中,并返回结果切片,也是类型S。值x被传递给类型为...T的参数,其中T是切片S的元素类型,并且适用相应的参数传递规则。作为特例,append还接受第一个参数可赋值给[]byte类型,第二个参数为string类型,后跟...。这种形式将字符串的字节追加到切片中。
如果切片s的容量不足以容纳额外的值,append会分配一个足够大的新底层数组,以容纳现有切片元素和额外的值。否则,append会重用底层数组。
copy函数将源切片src的元素复制到目标切片dst,并返回复制的元素数。两个参数必须具有相同的元素类型T,并且必须可赋值给类型[]T的切片。复制的元素数是len(src)和len(dst)中的最小值。作为特例,copy还接受目标参数可赋值给[]byte类型,源参数为字符串类型。这种形式将字符串的字节复制到字节切片中。
以上是关于append和copy函数的一些示例用法和说明。
另外,代码示例是一个使用append和copy函数的示例程序,用于演示如何在不覆盖输入的情况下进行切片操作。它创建了一个名为args的字符串切片,并使用一个名为funcArg的切片作为输出。通过迭代args切片,使用copy函数将args中的元素复制到funcArg中,并打印出结果。最后,打印出原始的args切片。
希望以上内容对你有帮助。如果还有其他问题,请随时提问。
<details>
<summary>英文:</summary>
&gt; [The Go Programming Language Specification][1]
&gt; 
&gt; [Appending to and copying slices][2]
&gt; 
&gt; The built-in functions append and copy assist in common slice
&gt; operations. For both functions, the result is independent of whether
&gt; the memory referenced by the arguments overlaps.
&gt; 
&gt; The variadic function append appends zero or more values x to s of
&gt; type S, which must be a slice type, and returns the resulting slice,
&gt; also of type S. The values x are passed to a parameter of type ...T
&gt; where T is the element type of S and the respective parameter passing
&gt; rules apply. As a special case, append also accepts a first argument
&gt; assignable to type []byte with a second argument of string type
&gt; followed by .... This form appends the bytes of the string.
&gt; 
&gt;     append(s S, x ...T) S  // T is the element type of S
&gt; 
&gt; If the capacity of s is not large enough to fit the additional values,
&gt; append allocates a new, sufficiently large underlying array that fits
&gt; both the existing slice elements and the additional values. Otherwise,
&gt; append re-uses the underlying array.
&gt; 
&gt;     s0 := []int{0, 0}
&gt;     s1 := append(s0, 2)                // append a single element     s1 == []int{0, 0, 2}
&gt;     s2 := append(s1, 3, 5, 7)          // append multiple elements    s2 == []int{0, 0, 2, 3, 5, 7}
&gt;     s3 := append(s2, s0...)            // append a slice              s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
&gt;     s4 := append(s3[3:6], s3[2:]...)   // append overlapping slice    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
&gt;     
&gt;     var t []interface{}
&gt;     t = append(t, 42, 3.1415, &quot;foo&quot;)   //                             t == []interface{}{42, 3.1415, &quot;foo&quot;}
&gt;     
&gt;     var b []byte
&gt;     b = append(b, &quot;bar&quot;...)            // append string contents      b == []byte{&#39;b&#39;, &#39;a&#39;, &#39;r&#39; }
&gt; 
&gt; The function copy copies slice elements from a source src to a
&gt; destination dst and returns the number of elements copied. Both
&gt; arguments must have identical element type T and must be assignable to
&gt; a slice of type []T. The number of elements copied is the minimum of
&gt; len(src) and len(dst). As a special case, copy also accepts a
&gt; destination argument assignable to type []byte with a source argument
&gt; of a string type. This form copies the bytes from the string into the
&gt; byte slice.
&gt; 
&gt;     copy(dst, src []T) int
&gt;     copy(dst []byte, src string) int
&gt; 
&gt; Examples:
&gt; 
&gt;     var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
&gt;     var s = make([]int, 6)
&gt;     var b = make([]byte, 5)
&gt;     n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
&gt;     n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
&gt;     n3 := copy(b, &quot;Hello, World!&quot;)  // n3 == 5, b == []byte(&quot;Hello&quot;)
Don&#39;t overwrite your input by using append. Use a separate variable for your output (function argument). For example,
package main
import &quot;fmt&quot;
func main() {
args := []string{&quot;2&quot;, &quot;3&quot;, &quot;8&quot;}
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)
}
Output:
[2 3 8]
[3 8]
[2 8]
[2 3]
[2 3 8]
[1]: https://golang.org/ref/spec
[2]: https://golang.org/ref/spec#Appending_and_copying_slices
</details>
# 答案4
**得分**: 2
你可以将切片想象成一个由固定大小的数组和一个计数器组成的结构体。切片的容量是底层数组的大小,而切片的长度是计数器的值。
`append` 函数的定义如下:`func append(slice []Type, elems ...Type) []Type`([godoc](https://godoc.org/builtin#append)),这意味着你将把 `elems` 可变参数追加到 `slice` 参数中。如果 `len(elems) + len(slice) > cap(slice)`,则底层数组需要被更大的数组替换(具有更大的容量),这在 Go 语言中意味着创建一个新的切片(因此有返回参数)。
在你的情况下,你没有超出切片的容量,只是修改了它的内容。
一个简单(尽管稍微有些丑陋)的技巧是将两个 `append` 嵌套到一个空切片中:
```go
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 函数...

英文:

You can imagine a slice as a struct composed of an array of fixed size and a counter for the number of elements in it. The capacity of the slice is the size of the underlying array, while the lenght of the slice is the counter.

Append is defined that way : func append(slice []Type, elems ...Type) []Type (godoc), which essentially means that you will be appending the elem variadic argument to the slice argument. If len(elems) + len(slice) &gt; cap(slice), then udnerlying array will need to be changed for a bigger one (with a bigger capacity), which mean (in go) a new slice (hence the return parameter).

In your case, you didn't exceeded the capacity of the slice. You just modified its content.

One simple (albeit slightly ugly) trick would be to nested two appends to an empty slice :

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

Or, if you want to pass a copy of the slice to a method (and do what you want after that), you can use the copy function…

答案5

得分: 1

现有的答案已经充分解释了原帖作者观察到的行为,并提供了可行的解决方案。然而,没有提到在Go 1.2(2012年末)引入的“完整切片表达式”(全切片表达式)(也称为“三索引切片”)。使用完整切片表达式作为append的第一个参数可以简洁地解决原帖作者的问题。为了完整起见,我在这里包含了这种方法:

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中的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

英文:

The existing answers adequately explain the behaviour observed by the OP and provide viable solutions. However, none mention full slice expressions (a.k.a. three-index slices) introduced in Go 1.2 (late 2012). Using one as the first argument of append solves the OP's problem in a concise manner. I'm including the approach here for completeness:

<!-- language: go --->

package main

import &quot;fmt&quot;

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

(Playground)

Output:

<!-- language: none --->

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

Since Go 1.18, you can use functions Clone and Delete from the golang.org/x/exp/slices package to achieve the same result in an arguably more readable fashion:

package main

import (
	&quot;fmt&quot;

	&quot;golang.org/x/exp/slices&quot;
)

func main() {
	args := []string{&quot;2&quot;, &quot;3&quot;, &quot;8&quot;}
	for i, _ := range args {
		fmt.Println(slices.Delete(slices.Clone(args), i, i+1))
	}
	fmt.Println(args)
}

(Playground)

huangapple
  • 本文由 发表于 2016年3月10日 23:22:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/35920534.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定