如果对象在切片中,则方法不会更改对象的值

时间:2018-07-24 05:03:58

标签: pointers for-loop go struct slice

这是我的程序:

package main

import (
    "fmt"
)

type Number struct {
    val int
}

func (num * Number) Increment () {
    num.val += 1
}

func (num Number) Value() int {
    return num.val
}

func main() {
    numbers := []Number {
        {val: 12}, 
        {val: 7},
        {val: 0},
    }

    for _, each := range numbers {
        each.Increment()
        fmt.Println(each.Value())
    }

    for _, each := range numbers {
        fmt.Println(each.Value())
    }
}

以下是输出:

13
8
1
12
7
0

第一个问题:为什么Increment()方法不更新第一个for循环中的值?我使用指针作为接收器,以便可以肯定地更新val,但是为什么第二个for循环会打印出这些Number的原始值?

第二个问题:如何做,以便当我遍历Number的一部分并调用Increment()方法时,所有Number s都正确地递增了?

[编辑] 我注意到,如果我使用基于索引的for循环并调用Increment()方法,则值将被正确更新。为什么?

for i := 0; i < len(numbers); i++ {
    numbers[i].Increment()
}

3 个答案:

答案 0 :(得分:2)

for range循环:

for _, each := range numbers {

numbers切片的元素上进行迭代,并在每次迭代中为each循环变量分配(副本)一个元素。

由于您的numbers切片的类型为[]Number,它将把Number结构复制到each变量中(其类型将为Number)。

然后在此变量上调用Number.Increment()方法。由于Increment()具有指针接收器,因此这是(&each).Increment()的简写。因此,此循环变量的地址被用作Increment()方法的接收器。 Increment()方法将正确更改此循环变量,但这是独立于切片的独立变量,因此您无需修改​​切片中的元素。

当您这样做:

for i := 0; i < len(numbers); i++ {
    numbers[i].Increment()
}

numbers的元素未在此处复制。这个:

numbers[i].Increment()

numbers切片建立索引,并且由于Increment()具有指针接收器,因此numbers[i]的地址被获取和使用,这是切片中元素的地址。因此,在这里,您将修改切片的Number结构值。

请注意,您也可以在此处使用for range

for i := range numbers {
    numbers[i].Increment()
}

遍及切片时的第一个迭代变量是索引。

此外,如果将指针存储在numbers切片(其类型为[]*Number)中,则将发生相同的情况,但是在这种情况下,for range将被复制指针,而不是结构,并且循环变量中的指针将指向与切片中的指针相同的Number结构值,因此也可以与您的第一个for range变体一起使用。

所有这些详细信息在Spec: For statements具有范围子句的语句小节中。

答案 1 :(得分:1)

在您的原始版本中,循环变量eachNumber结构的副本。请注意,它不是指向它的指针,也不是指向它的指针的副本。这意味着,每个迭代中都有一个新创建的Number。您在指向该新创建实例的指针上调用方法,然后在循环且原始数据未更改后被销毁。

如果您改为使用numbers := []*Number { ...并对其进行迭代,则each变量将是指向Number的指针的副本。使用指针副本与使用指针副本相同,因为副本指向相同的内存位置,因此,如果您随后调用该方法,则切片中的数据将会更改。

进行编辑:显然,如果使用numbers[i],则将引用切片中的数据。如上所述,for range循环将在其each变量中创建项目的副本。

答案 2 :(得分:0)

无论何时遍历切片,它都会创建原始变量的副本,该副本用作值并递增。但是,当您使用index时,您所指向的是该地址上支持的值,该值将被递增,因此原始值将被更改。

打印两个变量的值,以查看更改为:

for i, each := range numbers {
    each.Increment()
    fmt.Println(each, numbers[i])
}

您还可以使用原始值地址循环打印变量的地址,以查看两个变量具有不同的地址。因此,当您在numbers上进行迭代时,实际上是在创建局部变量。

for i, each := range numbers {
    each.Increment()
    fmt.Printf("%p -- %p\n",&each, &numbers[i])
}

用于检查Go Playground上地址的工作代码