スライスの先頭要素に値を追加するレシーバを作成する際に、ポインタ関連で詰まったので、その時の失敗例と成功例、その理由を書いておく。
やりたかったこと
以下のように、スライスの先頭に値を追加するレシーバを作成したい。
type Numbers []int
func (numbers Numbers) AddToHead(num int) {
// 省略
}
func main() {
nums := Numbers{0}
fmt.Println(nums) // [0]
nums.AddToHead(5)
fmt.Println(nums) // [5 0]
}
失敗例
パターン1
package main
import "fmt"
type Numbers []int
func (numbers Numbers) AddToHead(num int) {
numbers = append(append(numbers[:0], num), numbers[0:]...)
}
func main() {
nums := Numbers{0}
fmt.Println(nums)
for i := 1; i < 5; i++ {
nums.AddToHead(i)
fmt.Println(nums)
}
}
これを実行すると
[0]
[1]
[2]
[3]
[4]
となってしまう。
理由
AddToHead()内で、以下の処理が行われているため、上記の結果が出力される。
- 値レシーバなので、
numsがnumbersにコピーされる。この時、&nums != &numbersだが、内部の要素は参照渡しとなるため、&nums[0] == &numbers[0]。 -
append(numbers[:0], num)が実行され、numbers[:0]の隣、すなわちnumbers[0]にnumが追加される。この時点で、nums[0] = numとなる。 -
append(append(numbers[:0], num), numbers[0:]...)が実行されるが、この後実行される処理はnumbersを対象に行われており、numbersとアドレスが違うnumsは影響を受けない。
上記の処理により、nums[0] = numに書き換えが行われただけで、numsに追加処理がされていないため、上記のような出力が得られる。
パターン2
package main
import "fmt"
type Numbers []int
func (numbers Numbers) AddToHead(num int) {
tmp := append(Numbers{num}, numbers[0:]...)
numbers = append(numbers[:0], tmp...)
}
func main() {
numbers := Numbers{0}
fmt.Println(numbers)
for i := 1; i < 5; i++ {
numbers.AddToHead(i)
fmt.Println(numbers)
}
}
これを実行すると
[0]
[0]
[0]
[0]
[0]
となってしまう。
理由
AddToHead()内で、以下の処理が行われているため、上記の結果が出力される。
- 値レシーバなので、
numsがnumbersにコピーされる。この時、&nums != &numbersだが、内部の要素は参照渡しとなるため、&nums[0] == &numbers[0]。 -
tmp := append(Numbers{num}, numbers[0:]...)が実行され、新たにNumbers{num}を生成し、それにnumbersの要素をコピーして追加する。 -
numbers = append(numbers[:0], tmp...)が実行されるが、この処理はnumbersを対象に行われており、numbersとアドレスが違うnumsは影響を受けない。(この処理が行われた時点で、numbers[0]のアドレス、すなわちnums[0]のアドレスも失われたので、このレシーバ内からはnumsに干渉できない。)
上記の処理から、numsに対して何も行われないため、上記のような出力が得られる。
成功例
以上の失敗から、原因を考えた結果できた成功例。
package main
import "fmt"
type Numbers []int
func (numbers *Numbers) AddToHead(num int) {
tmp := append(Numbers{num}, (*numbers)[0:]...)
*numbers = append((*numbers)[:0], tmp...)
}
func main() {
numbers := Numbers{0}
fmt.Println(numbers)
for i := 1; i < 5; i++ {
numbers.AddToHead(i)
fmt.Println(numbers)
}
}
これを実行すると、
[0]
[1 0]
[2 1 0]
[3 2 1 0]
[4 3 2 1 0]
の出力が得られ、目的を達成していることがわかる。
失敗例のパターン2から「値レシーバからポインタレシーバに変更」のみ変更を加えたもの。
理由
AddToHead()内で、以下の処理が行われているため、上記の結果が出力される。
- ポインタレシーバなので、
numsのアドレスがnumbersにコピーされる。 -
tmp := append(Numbers{num}, (*numbers)[0:]...)が実行され、新たにNumbers{num}を生成し、それに(*numbers)[0:]の要素をコピーして追加している。numsとnumbsersが同一のアドレスなためnums[0:]の要素を追加していることになる。 -
numbers = append(numbers[:0], tmp...)が実行され、numbersの先頭にtmpの要素を追加する。この処理はnumbersを対象に行われているが、同一アドレスのnumsも変更されている。
上記の処理から、numsに値が追加され上記のような出力が得られる。