为了账号安全,请及时绑定邮箱和手机立即绑定

为什么我无法使用引用将值附加到结构的切片?

为什么我无法使用引用将值附加到结构的切片?

Go
精慕HU 2023-08-07 16:38:36
我去,我假设切片是通过引用传递的,但这似乎适用于值,但不适用于数组本身。例如,如果我有这个结构:    l := Line{        Points: []Point{            Point{3, 4},        },    }我可以定义一个变量,它传递对结构切片的引用slice := l.Points然后,如果我修改它,变量引用的原始结构将反映这些修改。slice[0].X = 1000fmt.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.Pointsarr[0].X = 1000fmt.Println(arr.[0].X != s.Points[0].X) // equals true, original struct is untouched那么,该l结构就不会被修改。现在,如果我想修改切片本身,我显然不能这样做:slice = append(slice, Point{99, 100})因为这只会重新定义切片变量,从而丢失原始引用。我知道我可以简单地这样做:l.Points = append(l.Points, Point{99, 100})但是,在某些情况下,使用另一个变量比键入整个变量更方便。我试过这个:*slice = append(*slice, Point{99, 100})但它不起作用,因为我试图取消引用显然不是指针的东西。我终于尝试了这个:slice := &l.Points*slice = append(l.Points, Point{99, 100})它有效,但我不确定发生了什么。为什么切片的值没有被覆盖?这里如何append运作?
查看完整描述

4 回答

?
绝地无双

TA贡献1946条经验 获得超4个赞

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

在谈论时,我们可以将引用一词的用法理解为显式指针,例如:


func f1(p *int) {

    // code ...

}

以及谈论时隐含的指针:


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

func f3(s []T) { ... }

在 中f1,preally 是一个指针:因此它指的是某个实际的int或 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.Pointsslice因此该变量的类型为*[]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})

仅当由于某种原因无法使用时才需要使用, 3但如果更方便的话也可以使用。读读读,更新更新。*slicel.Points*slice*slicel.Points*slicel.Points


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

var s []int

对比:

var s = []int{42}

第一个离开,s == nil而第二个创建一个底层数组,该数组能够保存一个int42,保存一个int值 42,因此s != nil

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

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

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

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

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


查看完整回答
反对 回复 2023-08-07
?
收到一只叮咚

TA贡献1821条经验 获得超4个赞

Append 返回一个新切片,该切片可能会修改初始切片的原始支持数组。原始切片仍将指向原始后备数组,而不是新数组(新切片可能位于内存中的同一位置,也可能不位于内存中的同一位置)

例如(操场

slice := []int{1,2,3}

fmt.Println(len(slice))

// Output: 3

newSlice := append(slice, 4)

fmt.Println(len(newSlice))

// Output: 4

fmt.Println(len(slice))

// Output: 3 

虽然切片可以被描述为“指向数组的胖指针”,但它不是指针,因此您无法取消引用它,这就是您收到错误的原因。

通过创建一个指向切片的指针,并append按照上面的操作使用,您可以将指针指向的切片设置为 . 返回的“新”切片append


查看完整回答
反对 回复 2023-08-07
?
梵蒂冈之花

TA贡献1900条经验 获得超5个赞

我知道这可能是亵渎的,但是对我来说,将切片视为结构很有用。


type Slice struct {

    len int

    cap int

    Array *[n]T // Pointer to array of type T

由于在像 C 这样的语言中,该[]运算符也是一个解引用运算符,因此我们可以认为每次访问切片时,我们实际上都在解引用底层数组并为其分配一些值。那是:


var s []int

s[0] = 1

可能被认为等同于(在伪代码中):


var s Slice

*s.Array[0] = 1 

这就是为什么我们可以说切片是“指针”。因此,它可以像这样修改其底层数组:


myArray := [3]int{1,1,1}

mySlice := myArray[0:1]

mySlice = append(mySlice, 2, 3) // myArray == mySlice

修改mySlice也会修改myArray,因为切片存储了指向数组的指针,并且在追加时,我们取消引用该指针。


然而,这种行为并不总是这样。如果超出原始数组的容量,则会创建一个新数组,并且原始数组保持不变。


myArray := [3]int{1,1,1}

mySlice := myArray[0:1]

mySlice = append(mySlice, 2, 3, 4, 5) // myArray != mySlice

当我们尝试将切片本身视为实际指针时,就会出现混乱。由于我们可以通过附加到底层数组来修改它,因此我们相信在这种情况下:


sliceCopy := mySlice

sliceCopy = append(sliceCopy, 6)

两个切片slice和sliceCopy是相同的,但它们不是。我们必须显式传递对切片内存地址的引用(使用&运算符)才能修改它。那是:


sliceAddress := &mySlice

*sliceAddress = append(mySlice, 6) // or append(*sliceAddress, 6)


查看完整回答
反对 回复 2023-08-07
?
ibeautiful

TA贡献1993条经验 获得超5个赞

您的第一次尝试没有成功,因为切片不是指针,它们可以被视为引用类型。如果底层数组有足够的容量,Append 将修改它,否则返回一个新切片。

通过结合这两种尝试,您可以实现您想要的目标。

操场

l := Line{

    Points: []Point{

        Point{3, 4},

    },

}


slice := &l.Points


for i := 0; i < 100; i++ {

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

}


fmt.Println(l.Points)


查看完整回答
反对 回复 2023-08-07
  • 4 回答
  • 0 关注
  • 104 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信