首页 > 解决方案 > 为什么我不能使用引用将值附加到结构的切片?

问题描述

我去,我假设切片是通过引用传递的,但这似乎适用于值,但不适用于数组本身。例如,如果我有这个结构:

    l := Line{
        Points: []Point{
            Point{3, 4},
        },
    }

我可以定义一个变量,它被传递给结构切片的引用

slice := l.Points

然后如果我修改它,变量引用的原始结构将反映这些修改。

slice[0].X = 1000
fmt.Printf(
    "This value %d is the same as this %d", 
    slice[0].X, 
    l.Points[0].X,
)

这与我认为是按值传递的数组的行为不同。因此,例如,如果我使用数组定义了前面的代码:

l := Line{
    Points: [1]Point{
        Point{3, 4},
    },
}
arr := l.Points
arr[0].X = 1000
fmt.Println(arr.[0].X != s.Points[0].X) // equals true, original struct is untouched

然后,该l结构不会被修改。

现在,如果我想修改切片本身,我显然不能这样做:

slice = append(slice, Point{99, 100})

因为那只会重新定义 slice 变量,从而丢失原始引用。我知道我可以简单地做到这一点:

l.Points = append(l.Points, Point{99, 100})

但是,在某些情况下,使用另一个变量而不是输入整个变量会更方便。

我试过这个:

*slice = append(*slice, Point{99, 100})

但它不起作用,因为我试图取消引用显然不是指针的东西。

我终于尝试了这个:

slice := &l.Points

*slice = append(l.Points, Point{99, 100})

它有效,但我不确定发生了什么。为什么 slice 的值没有被覆盖?如何在append这里工作?

标签: pointersgoslice

解决方案


让我们首先放弃一个术语问题。Go 语言规范并没有像您使用它的方式那样使用引用这个词。然而,Go 确实有指针,而指针是一种引用形式。此外,切片和映射有点特殊,因为有一些底层数据——切片下面的数组,或者映射的存储空间——可能已经存在也可能不存在,或者是通过声明或定义类型为or for的变量创建的一些类型T或类型对T1T21slice of Tmap[T1]T2

在谈论时,我们可以将您对引用一词的使用表示显式指针,例如:

func f1(p *int) {
    // code ...
}

以及谈论时的隐含指针:

func f2(m map[T1]T2) { ... }
func f3(s []T) { ... }

In f1, preally 是一个指针:因此它指的是一些实际int的 , or is nil。在f2中,m指的是一些底层地图,或者是nil。在f3,s是指一些底层数组,或者是nil

但是如果你写:

l := Line{
    Points: []Point{
        Point{3, 4},
    },
}

那么你一定写过:

type Line struct {
    // ... maybe some fields here ...
    Points []Point
    // ... maybe more fields here ...
}

Line是一个结构类型。它不是切片类型;它不是地图类型。它包含一个切片类型,但它本身不是一个。

您现在谈论传递这些切片。如果您通过l,则您将按值传递整个struct值。区分它并传递l.Points. 接收这些参数之一的函数必须使用正确的类型声明它。

因此,在大多数情况下,谈论参考只是一个转移注意力的方法——分散了对实际情况的注意力。我们需要知道的是:你给什么变量赋值,使用什么源代码?

有了所有这些,让我们谈谈您的实际代码示例:

l.Points = append(l.Points, Point{99, 100})

这就是它所说的:

  • 传递l.Pointsto append,这是一个内置的,因为它有点神奇的类型灵活(与 Go 的其余部分相比,类型非常严格)。它接受任何类型的值( T的切片,对于任何有效的类型T)加上一个或多个类型的值,并产生一个相同类型的新值,。[]TT[]T

  • 将结果分配给l.Points

它什么append时候工作,它可能:

  • 接收nil(给定类型的):在这种情况下,它创建底层数组,或者
  • 接收一个非零切片:在这种情况下,它会写入底层数组或丢弃该数组,以便根据需要使用新的更大容量的数组。2

因此,在所有情况下,底层数组实际上可能刚刚被创建或替换。因此,对同一底层数组的任何其他使用进行适当更新是很重要的。将结果分配回更新(可能是唯一的)切片变量,该变量引用底层数组。l.Points

然而,我们可以打破这些假设:

s2 := l.Points

现在l.Pointss2两者都引用(单个)底层数组。修改该底层数组的操作至少会潜在地影响s2 l.Points

你的第二个例子本身是好的:

*slice = append(*slice, Point{99, 100})

但是您还没有展示它自己是如何 slice被声明和/或分配给的。

你的第三个例子也很好:

slice := &l.Points
*slice = append(l.Points, Point{99, 100})

这些行中的第一行声明并初始化slice为指向l.Points. 因此变量slice具有类型*[]Point。它的值——即 中的值slice,而不是 中的值*slice——是 的地址l.Points,其类型为[]Point

中的值*slice是 中的值l.Points。所以你可以写:

*slice = append(*slice, Point{99, 100})

这里。因为*slice只是 的另一个名字l.Points,你也可以写成:

l.Points = append(*slice, Point{99, 100})

需要*slice有某种原因l.Points不可用的情况下使用,3但如果这样更方便,你可以使用。*slice阅读*slice阅读l.Points和更新*slice更新l.Points


1要了解我所说的可能或可能不会在此处创建,请考虑:

var s []int

与:

var s = []int{42}

第一个离开s == nil,而第二个创建一个能够保存一个int值的底层数组42,保存一个int值 42,所以s != nil.

2我不清楚是否承诺永远不会在容量大于其当前长度但不足以容纳最终结果的现有切片数组上写入。也就是可以append先追加10个对象到已有的底层数组,然后发现需要更大的数组,然后扩展底层数组吗?如果存在引用现有底层数组的其他切片值,则可以观察到差异

3在这里,如果您有理由传递l.Points或传递&l.Points给某些现有(预先编写的)函数,则会出现一个经典示例:

  • 如果您需要将l.Points切片值传递给某个现有函数,则该现有函数无法更改切片值,但可以更改底层数组。这可能是一个糟糕的计划,所以如果它确实这样做了,请确保这没问题!如果它只读取切片和底层数组,那就更安全了。

  • 如果您需要将&l.Points一个指向切片值的值传递给某个现有函数,则该现有函数可以更改切片和底层数组。

如果您正在编写一个新函数,则由您决定以最合适的方式编写它。如果你只打算读取切片和底层数组,你可以取一个 type 的值[]Point。如果您打算就地更新切片,您应该获取一个类型的值——*[]Point指向切片的指针Point


推荐阅读