スライスの先頭要素に値を追加するレシーバを作成する際に、ポインタ関連で詰まったので、その時の失敗例と成功例、その理由を書いておく。
やりたかったこと
以下のように、スライスの先頭に値を追加するレシーバを作成したい。
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
パターン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
パターン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
に値が追加され上記のような出力が得られる。