LoginSignup
6
3

More than 1 year has passed since last update.

Go言語 — slice と array とポインタ

Last updated at Posted at 2017-03-27

まとめ

  • スライス は 配列 のラッパーのようなもの。
  • スライス への操作をおこなうと、内部ではいい感じで 配列を操作してくれる。 スライス はその 配列 を参照する。
  • スライス という名前には 「配列の断片を使う」というニュアンスがあるように感じられる。
    • この動作を便宜的に「要素数を気にしないタイプの配列」として扱えるイメージ。
  • 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) という構造になっているが、

最初のポインタが

  • どの配列を参照するか
  • 配列の何番目から何番目までを参照するか

を覚えていてくれている。

image

( 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

感想

  • 低級言語の仕組みを知れて良かった。
  • 当たり前だが高級言語でも内部的には、こういったことがおこなわれているんだろうなと思った。
  • 配列の扱いひとつでも、メモリ使用量とかを気にする気持ちが少し分かった気がする。

参考

チャットメンバー募集

何か質問、悩み事、相談などあればLINEオープンチャットもご利用ください。

Twitter

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3