Method does not change the value of object if the object is in a slice

笑着哭i 提交于 2021-02-08 10:19:41

问题


Here is my program:

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())
    }
}

Here is the output:

13
8
1
12
7
0

First question: why does the Increment()method not update the value in the first for loop? I used pointer as the receiver so that val can be updated for sure, but why would the second for loop print out the original values of those Numbers?

Second question: what can be done so that when I iterate over a slice of Numbers and invoke the Increment() method, all Numbers are correctly incremented?

[Edit] I noticed that if I use index-based for loop and invoke the Increment() method, values will be correctly updated. Why?

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

回答1:


This for range loop:

for _, each := range numbers {

iterates over the elements of the numbers slice, and in each iteration it assigns (copies) an element to the each loop variable.

Since your numbers slices is of type []Number, it will copy the Number struct into the each variable (whose type will be Number).

Then you call the Number.Increment() method on this variable. Since Increment() has pointer receiver, this is a shorthand for (&each).Increment(). So the address of this loop variable is taken and used as the receiver for the Increment() method. The Increment() method will properly change this loop variable, but this is independent, distinct, detached from the slice, so you are not modifying the element in the slice.

When you do:

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

Elements of numbers are not copied here. This:

numbers[i].Increment()

Indexes the numbers slice, and since Increment() has a pointer receiver, the address of numbers[i] is taken and used, which is the address of the element in the slice. So here, you will modify the Number struct value of the slice.

Note that you can also use for range here:

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

The first iteration variable when ranging over a slice is the index.

Also, if you would store pointers in your numbers slice (which would then have type of []*Number), the same thing would happen, but in that case the for range would copy pointers, not structs, and the pointer in the loop variable would point to the same Number struct value as the pointer in the slice would, so that would also work with your first for range variant.

All these are detailed in Spec: For statements, in the For statements with range clause subsection.




回答2:


In your original version, the loop variable each is a copy of the Number struct. Notice that it is not a pointer to it, nor is it the copy of a pointer to it. That means, that there is a newly created Number in each iteration. You call a method on a pointer to this newly created instance, then it gets destroyed after the loop and your original data has not changed.

If you would instead use numbers := []*Number { ... and iterate over that, the each variable would be the copy of a pointer to Number. Using copies of pointers is the same as using pointers, because the copy points to the same memory location, thus if you then call the method, the data in the slice wil change.

For your edit: if you use numbers[i] then you reference the data inside the slice, obviously. As I stated above, the for range loop will create a copy of the items in its each variable.




回答3:


Whenever you loop through the slice it creates a copy of original variable which is used as value and is incremented. But when you are using the index you are pointing to the value backed at that address which is than incremented and hence original value got changed.

Print the value of both variable to see the changes as:

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

you can also print the address of variable in a loop with the original value address to see that both variables have different address. Hence you are actually creating a local variable when iterating over numbers.

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

Working Code to check address on Go Playground



来源:https://stackoverflow.com/questions/51490801/method-does-not-change-the-value-of-object-if-the-object-is-in-a-slice

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!