Go言語 — slice と array とポインタ
まとめ
- スライス は 配列 のラッパーのようなもの。
- スライス への操作をおこなうと、内部ではいい感じで 配列を操作してくれる。 スライス はその 配列 を参照する。
- スライス という名前には 「配列の断片を使う」というニュアンスがあるように感じられる。
- この動作を便宜的に「要素数を気にしないタイプの配列」として扱えるイメージ。
- Gopher の間では「スライスだけで良いじゃん」という説もあるようだ。
検証
1. 配列をスライスする
配列から一部の要素だけをスライスしてみる。
(スライスっていう名前通りの使い方)
array := [3]string {"Alice", "Bob", "Carol"}
slice := array[:2]
一部の要素だけをスライスしたので、得られる要素数は、スライスのほうが一個少ない。
配列にはCarorlがいるが、スライスにはいない。
fmt.Println(array) // [Alice Bob Carol]
fmt.Println(slice) // [Alice Bob]
2. スライスを変更する
Alice を Dave に変えてみる。
slice[0] = "Dave"
配列とスライスの両方で、Alice が Dave に変わった。
スライスは実体ではなく、配列を参照しているからだ。
fmt.Println(array) // [Dave Bob Carol]
fmt.Println(slice) // [Dave Bob]
「スライスの基底となる配列」のポインタを見ることも出来る。
underlying := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
fmt.Println(underlying) // 例: &{842350559520 2 3}
3. 配列を変更する
今度はスライスではなく、配列側の値を変更してみる。
array[1] = "Eric"
配列とスライスの両方で、Bob が Eric に変わった。
fmt.Println(array) // [Dave Eric Carol]
fmt.Println(slice) // [Dave Eric]
引き続きスライスは同じポインタで配列を参照している。
fmt.Println(underlying) // 例: &{842350559520 2 3}
4. スライスに要素を追加する
2個の要素が得られているスライスに、3個目の要素を追加してみる。
slice = append(slice, "Frank")
配列の3個目の要素が変わった。 Carol は Frank になった。
fmt.Println(array) // [Dave Bob Frank]
fmt.Println(slice) // [Dave Bob Frank]
スライスの cap の値が 2 から 3 に増えているが、ポインタは変わっていない。
fmt.Println(underlying) // 例: &{842350559520 3 3}
5. 配列の要素数を越える
Go での配列は、要素数の決まった型だ。
つまりこの例でいうと、配列は3個の string しか持っていない。 ( [3]string
という型 )
スライスの末尾にさらに要素を追加してみる。
slice = append(slice, "Greg")
- 配列の要素数は 3個
- スライスはその配列を参照している
と考えると、スライスに4個目の要素は追加できないはずだが?
結果を見てみると、今度は配列とスライスで得られる値が変わっている。
スライスにだけ Greg が追加されている。
fmt.Println(array) // [Dave Eric Frank]
fmt.Println(slice) // [Dave Eric Frank Greg]
試しに、スライスの1個目の要素を変更してみる。
slice[0] = "Henry"
そうすると、また配列は変更されていない。
スライスでだけ Dave が Henry に変わっている。
fmt.Println(array) // [Dave Eric Frank]
fmt.Println(slice) // [Henry Eric Frank Greg]
ポインタの状態を見てみると、ポインタの参照先が変わっている。
fmt.Println(underlying) // 例: &{842350895200 4 6}
配列とスライスの「リンク」が切れた状態になったようだ。
goのスライスは、今回のように【配列の要素数を越えるスライス】を作った場合、内部的に新しい配列を作り、今度はそいつを参照するようになるようだ。
pointer / len / cap
ちなみに、スライスは ポインタ / 長さ(len) / キャパシティ(cap)
という構造になっているが、
最初のポインタが
- どの配列を参照するか
- 配列の何番目から何番目までを参照するか
を覚えていてくれている。
( Go Slices: usage and internals - The Go Blog より )
- len はスライスで得られる要素数
- cap は基底となる配列の要素数
っぽい。
The length of a slice is the number of elements it contains.
The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.
( A Tour of Go より )
ただコードの最後の例で cap が 3 から 6 に増えているのは謎なので、今後調べたい。
コード全体
package main
import(
"fmt"
"reflect"
"unsafe"
)
func main() {
array := [3]string {"Alice", "Bob", "Carol"} // 配列を作る
slice := array[:2] // 配列から二個の要素をスライスする
fmt.Println(array) // [Alice Bob Carol]
fmt.Println(slice) // [Alice Bob]
slice[0] = "Dave" // スライスの先頭を変更する
fmt.Println(array) // [Dave Bob Carol] // 配列の値が変更される
fmt.Println(slice) // [Dave Bob] // スライスは配列を参照している
underlying := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) // スライスのポインタを見てみる
fmt.Println(underlying) // 例: &{842350559520 2 3}
array[1] = "Eric" // スライスではなく、配列側の中身を変更してみる
fmt.Println(array) // [Dave Eric Carol] // Bob が Eric に変わった
fmt.Println(slice) // [Dave Eric] // スライスは配列を参照しているので、スライスで得られる値も変わる
fmt.Println(underlying) // 例: &{842350559520 2 3} // ポインタが同じ
slice = append(slice, "Frank") // スライスの末尾に要素を追加する // 得られる要素数は三個になる
fmt.Println(array) // [Dave Bob Frank] // 配列の三個目の要素が変更された
fmt.Println(slice) // [Dave Bob Frank] // スライスは配列の要素全てを参照するようになった
fmt.Println(underlying) // 例: &{842350559520 3 3}
slice = append(slice, "Greg") // スライスの末尾にさらに要素を追加する // 参照先の配列の要素数を越えて4個になりそうだが?
fmt.Println(array) // [Dave Eric Frank] // 今度は配列の状態は変わっていない
fmt.Println(slice) // [Dave Eric Frank Greg] // スライスで得られる要素数は四個になった // スライスは配列を参照していたはずでは?
fmt.Println(underlying) // 例: &{842350895200 4 6} // ポインタの参照先が変わっている
slice[0] = "Henry" // スライスの最初の要素を変更する
fmt.Println(array) // [Dave Eric Frank] // 今度も配列の状態は変わっていない
fmt.Println(slice) // [Henry Eric Frank Greg] // スライスで得られる値は期待通りに変わっている
fmt.Println(underlying) // 例: &{842350895200 4 6} // ポインタの参照先が変わっている
}
環境
- go version go1.8 darwin/amd64
感想
- 低級言語の仕組みを知れて良かった。
- 当たり前だが高級言語でも内部的には、こういったことがおこなわれているんだろうなと思った。
- 配列の扱いひとつでも、メモリ使用量とかを気にする気持ちが少し分かった気がする。
参考
- Go Slices: usage and internals - The Go Blog
- The curious case of Golang array and slices
- How to get the underlying array of a slice in Go? - Stack Overflow
チャットメンバー募集
何か質問、悩み事、相談などあればLINEオープンチャットもご利用ください。
公開日時
2017-03-27
Discussion