首页 > 解决方案 > Golang 切片拷贝

问题描述

我不明白最后一步的结果,我认为“复制”使用值。如果您能解释一下,我将不胜感激。

type Vertex struct {
    X []int
    Y []int
}

func main() {
    var v Vertex
    x := []int{1, 2}
    y := []int{3, 4}
    v.X = x
    v.Y = y
    fmt.Println(v)

    x[0] = 5
    fmt.Println(v)

    copy(v.X, x)
    copy(v.Y, y)
    fmt.Println(v)

    x[0] = 6
    fmt.Println(v)
}

结果:

{[1 2] [3 4]}
{[5 2] [3 4]}
{[5 2] [3 4]}
{[6 2] [3 4]}

标签: go

解决方案


我不完全清楚你的误解在哪里:是切片本身,还是copy. 所以这有点长。

根据您要如何看待它(注意:这两种方式中只有一种是真正正确的),切片由两个(正确)或三个(错误,但在这里可能对您有所帮助)部分组成。让我们从错误的开始,注意到它是错误的,然后修复它:

  • 有你的变量,比如v.X,它可以是nil;
  • 然后,如果您的变量为零,则内存中的某处有一个切片标头,并且您的变量包含该标头;
  • 并且,如果标头本身是好的,它会保存一个指向内存中某处存在的底层数组的指针。

这有什么问题很简单:你的变量总是包含一些标题。只是您的变量持有的标头可以比较等于 nil。如果是这样,该语言不会让您继续使用它的其余部分。所以正确的版本是:

  • 您有某个类型的类型变量。这个变量要么nil,要么不是nil。您可以使用例如.[] TTv.X == nil

  • 如果变量为零,则变量本身包含切片头。保存该切片中数组位于其他位置。

现在让我们回到您的代码:

var v Vertex

这将创建struct-valued 变量v。它里面有两个切片变量,v.Xv.Y; 两者都是nil

x := []int{1, 2}

这是以下的简写:

var x []int
x = []int{1, 2}

第一行创建了保存标头的变量(最初为 nil,但编译器可能会跳过 set-to-nil-step,因为它不需要这样做)。第二行在某处创建一个双元素数组,用andint填充该数组,然后在变量中设置切片头以保存切片值:12x

  • 指向包含 {1, 2} 的数组的指针;
  • 长度,2;和
  • 容量,2。

现在v.X是一个有效的切片头,编译器已经构建了一个底层数组(某处)。

y := []int{3, 4}

我们现在对变量做同样的事情y。它最终持有一个切片头,具有三个值:

  • 指向包含 {3, 4} 的数组的指针;
  • 长度 2;和
  • 容量 2。

现在我们有:

v.X = x
v.Y = y

这两行将切片标头从 variable复制xv.X,从 variable复制yv.Y。两组切片标头现在匹配。还有两个底层数组:一个保存 {1, 2},另一个保存 {3, 4}。

fmt.Println(v)

Println函数内置了许多复杂的代码:它检查v,发现它是一个struct类型,检查元素v以查找v.Xv.Y,检查它们以发现它们是[]int类型,检查它们的标题是否为空,发现它们不是零,并打印出你所看到的。

x[0] = 5

这使用x切片头中的值来查找包含 {1, 2} 的数组,然后将 1 替换为 5。所以现在,底层数组包含 {5, 2}。里面的切片头x没有改变,当然里面的切片头v.X也没有改变。这两个切片标头仍然将长度和容量列为 2,并且都仍然指向单个底层数组。

fmt.Println(v)

这重复了相当复杂的工作,这次打印了[5 2]你看到的值。

copy(v.X, x)

copy函数需要两个切片头作为参数。1一个,destination dst,必须是一个非零切片才能让任何有用的事情发生。2 第二个, source src,可以是 nil 或非 nil。两者必须具有相同的基础类型。copy然后是:

  • 找到 和 中的较小len(dst)len(src)
  • 将许多元素从底层数组复制src到底层数组dst;和
  • 返回那个号码。

所以我们找到len(v.X), 是 2,并且len(x), 这也是 2,因此会将 2从底层数组copy复制到底层数组。intxv.X

这两个数组是同一个数组。因此,这会将元素复制回自身,而不会改变任何内容。该copy函数现在返回 2,而不更改任一切片的长度或容量。

换句话说,v.Xor都没有发生x任何事情,基础数组中也没有任何变化。

您现在对 重复这项工作v.Y, y,这又没有任何改变。两个切片头共享相同的底层数组,所以这个副本确实复制了两个元素,但没有任何改变。

其余代码以显而易见的方式进行。

如果您想制作某个切片的副本,您应该:

  • 找到原始切片的长度;
  • 创建一个该长度的切片;和
  • 复制到切片中。

在这种情况下,这将是,例如:

tmp := make([]int, len(x))
copy(tmp, x)
v.X = tmp

tmp变量不是必需的:您可以改为编写:

v.X = make([]int, len(x))
copy(v.X, x)

但是您可以定义一个小的分配和复制函数并使用它:

func copyIntSlice([]int a) []int {
    tmp := make([]int, len(a))
    copy(tmp, a)
    return tmp
}

然后使用:

v.X = copyIntSlice(x)

例如,这也使它成为单行v.Y

v.Y = copyIntSlice(y)

make是实际创建新数组的步骤。给定一些初始的底层数组,您可以随意摆弄切片头,但在某些时候,您可能需要创建一个新数组,否则您的所有切片都在使用旧的(共享)底层数组。


1第二个参数可以是字符串,而不是切片。字符串就像一个字节切片,但缺少单独的容量字段。幸运的是,copy没有使用第二个参数的容量;这就是让这一切最终得以解决的原因。

2如果第一个参数copy为 nil,则不copy执行任何操作。调用它并没有,它只是没有任何事情。如果这是正确的做法——如果我们不应该为这个案子做任何事情——那很好。


推荐阅读