Получатель значения структуры

Может ли кто-нибудь объяснить мне, почему вывод r.a пуст, поскольку я добавил в список значение 2?

package main

import (
    "fmt"
)

func main() {
    var r R
    r.b = make(map[int]int)

    r.add()
    fmt.Println(r) // outputs {[] map[2:2]}
}

type R struct {
    a []int
    b map[int]int
}

func (r R) add()  {
    r.a = append(r.a, 2)
    r.b[2] = 2
}

person zhuang    schedule 27.11.2018    source источник
comment
Получатель значения означает, что add получает копию содержимого r, а не указатель на r. Внутри add r.a заменяется обновленным значением среза, указывающим на другую память, включающую новый 2, но ничего не меняется в копии main r. Когда операции среза, подобные этой do, работают, например, когда Read сохраняет данные в байтовый срез, это происходит потому, что две копии слайса уже указывают на одну и ту же память, а операция изменения слайса просто изменила уже имеющуюся. -Общая память. Это введение в работу срезов может помочь: blog.golang.org/slices   -  person twotwotwo    schedule 28.11.2018
comment
Что касается другой связанной литературы, Расс Кокс написал пост о том, как работают срезы, которые могут оказаться полезными, и я ответил < href="https://stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values/23551970">вопрос об указателях и значениях.   -  person twotwotwo    schedule 28.11.2018
comment
@twotwotwo Большое спасибо. Это введение фрагмента действительно потрясающе blog.golang.org/slices. Концепция sliceHeader проясняет ситуацию.   -  person zhuang    schedule 28.11.2018
comment
Отлично, рад, что смог помочь!   -  person twotwotwo    schedule 28.11.2018


Ответы (1)


В коротком отрывке из Tour of Go говорится, что:

Методы с получателями указателей могут изменять значение, на которое указывает получатель [...]. Поскольку методам часто требуется модифицировать получателя, приемники указателей более распространены, чем приемники значений.

Почему r.b отображается правильно, а r.a вообще не изменяется?

Как уже говорилось в моем ответе ниже, ваш метод add() является приемником значений. Поэтому он возьмет вашу инициализированную структуру (т.е. r), скопирует ее, а затем соответствующим образом изменит. Поскольку вы инициализировали новую карту под r.b в своей функции main(), здесь копируется только ссылка на эту карту, но не вся карта. Поэтому манипуляция на карте работает, а на слайсе r.a нет. Но почему r.a совсем не меняется? Это связано с тем, что append(), расположенный в методе add(), хранит новый заголовок фрагмента под вашим свойством a и указывает на другой раздел базового массива. В конце концов, ваш метод получателя значения add() сделал копию r, установил новый заголовок среза в свойстве a и никогда не изменял исходную структуру r, которая была определена в функции main(), так как она была скопирована с помощью метода получателя значения. add().

В вашем случае метод add() является так называемым методом получателя значений, который не может выполнять какие-либо манипуляции с определенной вами структурой r, расположенной в функции main(), напрямую, но копирует ее и впоследствии выполняет манипуляции. Поэтому вам нужно превратить ваш метод add() в метод получателя указателя, как это:

func (r *R) add()  {
    r.a = append(r.a, 2)
    r.b[2] = 2
}

Теперь метод берет фактическую ссылку на вашу структуру r, которая инициируется в функции main(), и соответствующим образом изменяет ее.

Как это могло бы работать без изменения метода add() на приемник указателя?

Вам просто нужно будет вернуть скопированную структуру в метод add() следующим образом:

package main

import (
    "fmt"
)

func main() {
    var r R
    r.b = make(map[int]int)
    fmt.Println(r.add()) // outputs {[2] map[2:2]}
}

type R struct {
    a []int
    b map[int]int
}

func (r R) add() R {
    r.a = append(r.a, 2)
    r.b[2] = 2
    return r
}
person bajro    schedule 27.11.2018
comment
Спасибо за подробный ответ. Мой ключевой момент заключается в том, почему обновление b (которое является картой) может быть отражено в выводе, а обновление a (которое является срезом) игнорируется. Я думаю, что r.b — это указатель на базовые данные карты, а r.a также является указателем на базовые данные среза. Пожалуйста, поправьте меня. - person zhuang; 28.11.2018
comment
Согласно спецификации языка golang, метод получателя значения работает с копией исходной структуры. В вашем случае метод add() копирует вашу структуру r, добавляет 2 к r.a, а также добавляет 2 к ключу 2 на карте r.b. Ключевым моментом здесь является то, что ссылка (то есть указатель) инициализированной карты копируется во вновь созданную структуру, поскольку ваш метод add() является приемником значения. Итак, в конце, после вызова add() для r, он создает новую структуру (путем копирования старой), но копирует с ней ссылку на инициализированную карту. Поэтому ваш код ведет себя так. - person bajro; 28.11.2018
comment
@zhuang вы правы в том, что срез является ссылочным типом, таким как карта, но добавление к срезу может создать новый срез с другим базовым массивом. Это сделано для того, чтобы срезы могли быть смежными в памяти. Вставка в карту не создает новую карту. - person Chris Drew; 28.11.2018
comment
Я отредактировал свой ответ, чтобы привести пример того, как это сделать с приемником указателя, а также с приемником значения. Лично я бы выбрал приемник указателя, поскольку он просто изменяет исходную структуру, но не копирует ее. - person bajro; 28.11.2018
comment
@Bajro @Chris Drew Большое спасибо. Кажется, я понимаю, что сейчас произошло. В функции add(), как сказали ваши геи, r действительно является копией структуры R, определенной в функции main(). r.a — это копия sliceHeader, выглядящая так: sliceHeader{Length: 0, Capacity: 10, ZerothElement: &iBuffer[0]}. - person zhuang; 28.11.2018
comment
Поэтому, когда мы делаем r.a[0] = 1, изменение будет отражено в исходном, потому что они используют один и тот же базовый массив, а r.a в функции main() также указывает на значение в ячейке 0. Когда мы делаем r.a = append(r.a, 2), r.a становится новым sliceHeader. Базовый массив может измениться одновременно. Но ничего не происходит с исходной функцией r.a в main(), потому что sliceHeader функции r.a в main() остается прежним. - person zhuang; 28.11.2018
comment
@zhuang Да, именно в этом суть. При использовании слайсов, а в данном случае с методом append(), вы просто передаете заголовок (т. е. sliceHeader), и он просто указывает на новый слайс исходного слайса. Таким образом, базовый слайс все еще существует, но вновь созданный слайс с append() указывает на новый заголовок слайса. Вот еще одна ссылка на сообщение в блоге, очень хорошо объясняющее срезы blog.golang.org/ go-slices-usage-and-internals - person bajro; 28.11.2018