Go言語のArrayとSliceについて
調べるたびに忘れるのでメモしておきます。
基本的にGo Slices: usage and internals に基いています。 というかほとんど直訳です。
Introduction
Sliceは他の言語のArrayに似ているが、違う部分がある。
Arrays
Slice型はGo言語におけるArray型の抽象的なものである。
Array型は長さ(length)と要素(element)の型を明らかにしたものである。
下の例で、[4]int
は4つのint
型からなるArray型である。
長さは型の一部分である([4]int != [5]int
)。
var a [4]int a[0] = 1 i := a[0] // i == 1
Go言語では、Array型はzero valueで初期化されていて、すぐに使える。
a[2] == 0
Go言語のArrayは値(value)であり、最初の要素へのポインターではない(C言語とは違う)。 つまり、Arrayを渡すときには要素のコピーとなるので、これを避けるためにはArrayへのポインターを渡す必要がある(あくまでもArrayへのポインターであり、Arrayそのものでない)。 Arrayはfieldではなく数値でインデックス化された構造体であると考えることも出来る。 Arrayは以下のように書ける。
b := [2]string{"Penn","Teller"}
また、要素数は省略できる。
b := [...]string{"Penn", "Teller"}
どちらのケースにおいても、b
の型は[2]string
である。
Slices
Arrayは必要だけど、柔軟やない。なんでぶっちゃけあんまGo言語では見ないよね。GopherならSlice使うよね。 Sliceマジ便利。
Sliceは[]T
と書く。T
はSliceの要素の型を示す(string
とかint
とか)。Arrayと違って要素数は指定しません。
Sliceの書き方はArrayとむっちゃ似てますが、要素数は入りません。
letters := []string{"a", "b", "c", "d"}
みんな大好きmake
でも作れるぜ!
func make([]T, len, cap) []T
make
ではSliceの型と長さ、オプションで収容数(capasity)を指定します。
var s []byte s = make([]byte, 5, 5) // s == []byte{0, 0, 0, 0, 0}
cap
を指定しなかった場合はlen == cap
となる。
SliceのZero valueはnil
であり、len
もcap
もnil
に対しては0を返す。
Sliceは"slicing"で既存のSliceやArrayから作ることもできる。 Slicingは以下のように記述する。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'} // b[1:4] == []byte{'o', 'l', 'a'}, bと同じ(メモリ)場所を共有する
SlicingはArrayに対しても行える。
x := [3]string{"日", "本", "語"} s := x[:] // xの場所を参照するSlice
Slice internals
SliceはArrayセグメントの記述子である。 SliceはArrayへのポインター、Arrayセグメントの長さ、容量から成る。
SliceへのSlicingは元のSliceへの参照なので、SlicingでできたSliceに対する変更は元のSliceにも適用される。
d := []byte{'r', 'o', 'a', 'b'} e := d[2:] // e == [byte]{'a', 'd'} e[1] = 'm' // e == []byte{'a', 'm'} // d == []byte{'r', 'o', 'a', 'm'}
Sliceはcap
で示した容量までは簡単に増やせるけど、それ以上に増やしたい場合はcopy
やappend
を使う(以下を参照)。
Growing slices (the copy and append functions)
Sliceの容量を増やすには、大きなSliceを作ってそっちに要素をコピーする方法がある。
これは、他の言語が動的Arrayを実装するときにも使われる手法である。
次に示す例は、元のSlices
の2倍の大きさのSliceを作り、そちらに元のSliceの要素をコピーする例である。
t := make([]byte, (cap(s)+1)*2) // +1 in case cap(s) == 0 for i := range s { t[i] = s[i] } s = t
これを簡単に行うためのbuilt-in関数func copy(dst, src []t) int
がある。
t := make([]byte, len(s), (cap(s)+1)*2) copy(t, s) s = t
よく行われる操作の一つに、ある値をあるSliceの最後に付け足すというものがある。以下の様な実装が考えられる。
func AppendByte(slice []byte, data ...byte) []byte { m := len(slice) n := m + len(data) if n > cap(slice) { newSlice := make([]byte, (n+1)*2) copy(newSlice, slice) slice = newSlice } slice = slice[0:n] copy(slice[m:n], data) return slice }
こんなふうに使える。
p := []byte{2, 3, 5} p = AppendByte(p, 7, 11, 13) // p == []byte{2, 3, 5, 7, 11, 13}
実際にはbuilt-inのappend
関数を使うのがよい。
append
関数を利用したFilter
関数の例を以下に示す。
// Filter関数。条件f(x)に該当するものだけをSliceに入れて返す。 func Filter(s []int, fn func(int) bool) []int { var p []int // == nil for _, v := range s { if fn(v) { p = append(p, v) } } return p }
A possible "gotcha"
上で示したように、re-slicingはsliceで示したArrayをコピーしない。 このため、本当はあるデータ中の一部だけが必要なのに、全てのデータをメモリに乗せてしまうことがある。
以下で示すFindDigits
関数は、データを全てメモリに読み込み、数値の表記を探してsliceで返す。
var digitRegexp = regexp.MustCompile("[0-9]+") func FindDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) return digitRegexp.Find(b) }
この関数は期待通り動くが、読み込んだ全てのデータがメモリ上に保持されたままになる。 返したsliceが使われなくなるまで、全てのデータをメモリ上に保持するのはもったいなさすぎる。
なので、以下の様なコードが望まれる。
func CopyDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) b = digitRegexp.Find(b) c := make([]byte, len(b)) copy(c, b) return c }
append
でもっと簡潔に書けるよね!これは読者の宿題にしておくよ!
私的な結論
SliceはArrayをもっと便利にしたもので、Go言語ではSliceを使いましょう!ということでしょうかね。
最後の問題はこれでいいのだろうか。あまり簡潔になっている気がしない……。
func AppendDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) var c []byte for _, elem := range ioutil.ReadFile(filename) { c = append(c, elem) } return c }