done is better than perfect

自分が学んだことや、作成したプログラムの記事を書きます。すべての記載は他に定める場合を除き個人的なものです。

Go言語のArrayとSliceについて

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であり、lencapnilに対しては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で示した容量までは簡単に増やせるけど、それ以上に増やしたい場合はcopyappendを使う(以下を参照)。

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
}