英文:
unexpected slice append behaviour
问题
我今天在Go代码中遇到了奇怪的行为:当我在循环中将elements
追加到slice
中,然后尝试根据循环的结果创建新的slices
时,最后一个append
会覆盖之前的appends
的slices
。
在这个特定的例子中,这意味着sliceFromLoop
的j
、g
和h
切片的最后一个元素不是分别是100
、101
和102
,而是始终是102
!
第二个例子 - sliceFromLiteral
的行为符合预期。
package main
import "fmt"
func create(iterations int) []int {
a := make([]int, 0)
for i := 0; i < iterations; i++ {
a = append(a, i)
}
return a
}
func main() {
sliceFromLoop()
sliceFromLiteral()
}
func sliceFromLoop() {
fmt.Printf("** NOT working as expected: **\n\n")
i := create(11)
fmt.Println("initial slice: ", i)
j := append(i, 100)
g := append(i, 101)
h := append(i, 102)
fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}
func sliceFromLiteral() {
fmt.Printf("\n\n** working as expected: **\n")
i := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("initial slice: ", i)
j := append(i, 100)
g := append(i, 101)
h := append(i, 102)
fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}
在阅读、挖掘和实验之后,我发现这个问题是由于slices
引用了相同的底层数组值而导致的,可以通过在追加任何内容之前将slice
复制到新的slice
中来解决,但这看起来相当犹豫不决。
有没有一种惯用的方法可以基于旧的切片创建许多新的切片,而不必担心更改旧切片的值?
英文:
I encountered weird behaviour in go code today: when I append elements
to slice
in loop and then try to create new slices
based on the result of the loop, last append
overrides slices
from previous appends
.
In this particular example it means that sliceFromLoop
j
,g
and h
slice's last element are not 100
,101
and 102
respectively, but...always 102
!
Second example - sliceFromLiteral
behaves as expected.
package main
import "fmt"
func create(iterations int) []int {
a := make([]int, 0)
for i := 0; i < iterations; i++ {
a = append(a, i)
}
return a
}
func main() {
sliceFromLoop()
sliceFromLiteral()
}
func sliceFromLoop() {
fmt.Printf("** NOT working as expected: **\n\n")
i := create(11)
fmt.Println("initial slice: ", i)
j := append(i, 100)
g := append(i, 101)
h := append(i, 102)
fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}
func sliceFromLiteral() {
fmt.Printf("\n\n** working as expected: **\n")
i := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("initial slice: ", i)
j := append(i, 100)
g := append(i, 101)
h := append(i, 102)
fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}
link to play.golang:
https://play.golang.org/p/INADVS3Ats
After some reading, digging and experimenting I found that this problem is originated in slices
referencing the same underlaying array
values and can be solved by copying slice
to new one before appending anything, however it looks quite... hesitantly.
What's the idomatic way for creating many new slices based on old ones and not worrying about changing values of old slices?
答案1
得分: 16
不要将append
分配给除它自身以外的任何变量。
正如你在问题中提到的,混淆是因为append
既改变了底层数组,又返回一个新的切片(因为长度可能会改变)。你可能会认为它会复制该后备数组,但实际上它只是分配一个指向它的新的slice
对象。由于i
从不改变,所有这些append
都会将backingArray[12]
的值更改为不同的数字。
与此相反,将元素append
到数组时,每次都会分配一个新的字面数组。
所以是的,你需要在对其进行操作之前复制切片。
func makeFromSlice(sl []int) []int {
result := make([]int, len(sl))
copy(result, sl)
return result
}
func main() {
i := make([]int, 0)
for ii := 0; ii < 11; ii++ {
i = append(i, ii)
}
j := append(makeFromSlice(i), 100) // 正常工作
}
切片字面量的行为解释是因为如果append
操作超过了后备数组的cap
,则会分配一个新的数组。这与切片字面量无关,而与超过cap
的内部工作原理有关。
a := []int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("len(a) %d, cap(a) %d\n", len(a), cap(a))
// len(a) 7, cap(a) 7
b := make([]int, 0)
for i := 1; i < 8; i++ {
b = append(b, i)
} // b := []int{1, 2, 3, 4, 5, 6, 7}
// len(b) 7, cap(b) 8
b = append(b, 1) // 任意数字,只要达到cap
i := append(b, 100)
j := append(b, 101)
k := append(b, 102) // 现在这些都按预期工作
英文:
Don't assign append
to anything other than itself.
As you mention in the question, the confusion is due to the fact that append
both changes the underlying array and returns a new slice (since the length might be changed). You'd imagine that it copies that backing array, but it doesn't, it just allocates a new slice
object that points at it. Since i
never changes, all those appends keep changing the value of backingArray[12]
to a different number.
Contrast this to append
ing to an array, which allocates a new literal array every time.
So yes, you need to copy the slice before you can work on it.
func makeFromSlice(sl []int) []int {
result := make([]int, len(sl))
copy(result, sl)
return result
}
func main() {
i := make([]int, 0)
for ii:=0; ii<11; ii++ {
i = append(i, ii)
}
j := append(makeFromSlice(i), 100) // works fine
}
The slice literal behavior is explained because a new array is allocated if the append would exceed the cap
of the backing array. This has nothing to do with slice literals and everything to do with the internals of how exceeding the cap works.
a := []int{1,2,3,4,5,6,7}
fmt.Printf("len(a) %d, cap(a) %d\n", a, len(a), cap(a))
// len(a) 7, cap(a) 7
b := make([]int, 0)
for i:=1; i<8, i++ {
b = append(b, i)
} // b := []int{1,2,3,4,5,6,7}
// len(b) 7, cap(b) 8
b = append(b, 1) // any number, just so it hits cap
i := append(b, 100)
j := append(b, 101)
k := append(b, 102) // these work as expected now
答案2
得分: 5
如果你需要复制一个切片,除了复制切片之外没有其他方法。你几乎永远不应该将append
的结果赋给除了append
的第一个参数以外的变量。这会导致难以找到的错误,并且在切片是否具有所需容量的情况下行为不同。
这不是一个常见的需要模式,但是如果你需要多次重复几行代码,那么你可以使用一个小的辅助函数:
func copyAndAppend(i []int, vals ...int) []int {
j := make([]int, len(i), len(i)+len(vals))
copy(j, i)
return append(j, vals...)
}
https://play.golang.org/p/J99_xEbaWo
英文:
If you need a copy of a slice, there's no other way to do it other than, copying the slice. You should almost never assign the result of append
to a variable other than the first argument of append
. It leads to hard to find bugs, and will behave differently depending on whether the slice has the required capacity or not.
This isn't a commonly needed pattern, but as with all things of this nature if you need to repeate a few lines of code multiple times, then you can use a small helper function:
func copyAndAppend(i []int, vals ...int) []int {
j := make([]int, len(i), len(i)+len(vals))
copy(j, i)
return append(j, vals...)
}
答案3
得分: 0
这里还有一种更简单的实现copyAndAppend
函数的方法:
func copyAndAppend(source []string, items ...string) []string {
l := len(source)
return append(source[:l:l], items...)
}
在这里,我们只需确保source
没有可用的容量,这样就会强制进行复制。
英文:
There is also a little bit simpler way to implement copyAndAppend
function:
func copyAndAppend(source []string, items ...string) []string {
l := len(source)
return append(source[:l:l], items...)
}
Here we just make sure that source has no available capacity and so copying is forced.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论