vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

GolangのSliceあれこれ

はじめに

突然ですが A Tour of Go 始めました。

https://tour.golang.org/list

とは言いつつも、さらっと読み流しつつ、へ~、ほ~と思っているだけではあるのですが。

その中で気になったこととして、 Slice があります。

ほかの言語にある機能とは少し違っているのか、 Slice について書かれた記事は結構あったりするのですが、
その辺も踏まえて、自分がわからなかったり気になった部分をまとめたいと思います。

Sliceについて

まずは Slice について簡単におさらい。

他の言語と同じように Golang にも配列があります。

Slice はこの配列に対する参照(ポインタ)を持つものです。

特徴として、 Slice はポインタと要素数 (Length) の他に容量 (Capacity) を持っており、
Capacity を超えない範囲であれば要素数を任意に変更できることが挙げられます。

Sliceを作る

Sliceを作る方法は三つあります。
(s1 は配列 originalArray に対する参照を持っていたりと中のデータはそれぞれ異なります)

package main

import "fmt"

func main() {
    // 元の配列
    originalArray := [5]int{0, 1, 2, 3, 4}

    // Sliceを作る1.
    s1 := originalArray[:]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]

    // Sliceを作る1-2. [low:high]で配列の一部を持つSliceを作ることも可能.
    s1b := originalArray[2:5]
    printSlice(s1b)                     // Length: 3 Capacity: 3 Value: [2 3 4]

    // Sliceを作る2.
    s2 := []int{1, 2, 3, 4, 5}
    printSlice(s2)                      // Length: 5 Capacity: 5 Value: [1 2 3 4 5]

    // Sliceを作る3.
    s3 := make([]int, 5, 5)
    printSlice(s3)                      // Length: 5 Capacity: 5 Value: [0 0 0 0 0]
}
func printSlice(v []int){
    fmt.Printf("Length: %d Capacity: %d Value: %v \n", len(v), cap(v), v)
}
  • s2, s3 ではそれぞれ Slice とは別に、その参照元となる配列が作成されます。
    この配列は、自分に対する参照がすべて無くなったら破棄されます。

LengthとCapacity

さて、 Slice で最初に引っかかったのが A Tour of Go > More types: structs, slices, and maps. > Slice length and capacity のところです。

ここでは Slice を 配列[low:high] として生成し、そこから Slice を再生成しています。

import "fmt"

func main() {
    // 1. 配列からSliceを生成.
    s1 := []int{0, 1, 2, 3, 4}
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]

    // 2. Lengthを0にする
    s1 = s1[:0]
    printSlice(s1)                      // Length: 0 Capacity: 5 Value: []

    // 3. Lengthを4にする
    s1 = s1[:4]                         // Length: 4 Capacity: 5 Value: [0 1 2 3]
    printSlice(s1)

    // 4. Capacityが3になる
    s1 = s1[2:]
    printSlice(s1)                      // Length: 2 Capacity: 3 Value: [2 3]
}
~省略~

2.で Length を 0 にした後、 3. を実行すると Length が 4 になります。

ところが、 3.をコメントアウトして 4. を実行すると例外が発生します。

また、 high 側を設定している 2.、3. では Capacity が 5 のまま変化していないのに対し、
4.では 3 に変化しています。

ここから、 low 側の指定によって Capacity は変化するが Length は変化しないこと、
high 側は Length が変化するが Capacity が変化しないことがわかります。

なお後述しますが、 Capacity は減らすことはできても増やすことはできません。

copyについて

Slice を複製したい場合。

同じポインタを持ったSliceを複製する

前述のとおり、 Slice は配列に対するポインタを保持しています。

下記のように同じ配列から Slice を生成した場合、インデックスは違っても対応する値が変更されます。

~省略~
func main() {
    // 配列からSliceを生成.
    originalArray := [5]int{0, 1, 2, 3, 4}
    printArray(originalArray)           // Length: 5 Value: [0 1 2 3 4]

    // 元の配列とLength、Capacityが同じSlice
    s1 := originalArray[:]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]

    // 配列の2~5番目でSliceを生成.
    s2 := originalArray[2:5]
    printSlice(s2)                      // Length: 3 Capacity: 3 Value: [2 3 4]

    // s1を複製(元の配列のポインタを持ったSliceをs1と同条件で生成)
    s3 := s1
    printSlice(s3)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]

    // Sliceから値を変更すると元の配列の値が変更される.
    s1[2] = 22
    printArray(originalArray)           // Length: 5 Value: [0 1 22 3 4]

    // インデックスは違っても対応する値が変更される
    s2[0] = 33
    printArray(originalArray)           // Length: 5 Value: [0 1 33 3 4]
}
~省略~
func printArray(v [5]int){
    fmt.Printf("Length: %d Value: %v \n", len(v), v)
}

なお Slice を複製する場合も、 Slice そのものが複製されるわけではなく、
元の Slice と同じポインタ、 Length 、Capacity を持った Slice が生成されるようです。

内容は同じで別のポインタを持ったSliceを生成する

今度は値は同じ、でも別の配列に対するポインタを持つ Slice を作りたい場合。

コピー元と Length が同じ Slice を作成し、 copy を使って値をコピーします。

~省略~
func main() {
    // 配列からSliceを生成.
    originalArray := [5]int{0, 1, 2, 3, 4}
    printArray(originalArray)           // Length: 5 Value: [0 1 2 3 4]

    // 元の配列とLength、Capacityが同じSlice
    s1 := originalArray[:]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]

    // s1 と同じLengthを持ったSliceを新規作成
    s2 := make([]int, len(s1))
    // 生成時点ではすべてデフォルト値
    printSlice(s2)                      // Length: 5 Capacity: 5 Value: [0 0 0 0 0]
    // s1の値をコピーする
    copy(s2, s1)                        // Length: 5 Capacity: 5 Value: [0 1 2 3 4]
    printSlice(s2)

    s1[0] = -1
    // s1の値を変更してもs2に影響はない
    printSlice(s2)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 4]
}
~省略~

明示的に生成しなおさないと同じポインタが使われてしまう、というのは注意が必要そうです。

appendについて

Slice の Capacity は増やせないと言ったな。あれは嘘だ。

Slice に値を追加したい場合、 append を使うことができます。

~省略~
func main() {
    originalArray := [5]int{0, 1, 2, 3, 4}
    printArray(originalArray)           // Length: 5 Value: [0 1 2 3 4]

    s1 := originalArray[0:4]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3]

    // 値を追加する1:  追加後のLengthがCapacityを超えない場合
    s1 = append(s1, 6)
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 6]

    // 値を追加する2: 追加後のLengthがCapacityを超える場合
    s1 = append(s1, 7)
    printSlice(s1)                      // Length: 6 Capacity: 10 Value: [0 1 2 3 6 7]
}
~省略~

注目しどころとしては、 append で値を追加すると Slice の最後に値が追加され、
Length が +1 されますが Capacity はそのままです。

ところが、 Length と Capacity の数が同じ状態で append すると、
Capacity は append 実行前の二倍になっています。

これは、 Capacity が Length の数より小さくなる場合に自動で Slice が作り直されるのですが、
一つ追加するごとに作り直すのは効率が悪いため、ということのようです。

この append したときの Length と Capacity の数によって Slice が作り直される、
という仕様でもう一つ驚いたことがありました。

それは、 Slice が作り直されると、元の配列へのポインタが保持されない、ということです。

~省略~
func main() {
    originalArray := [5]int{0, 1, 2, 3, 4}
    printArray(originalArray)           // Length: 5 Value: [0 1 2 3 4]

    s1 := originalArray[0:4]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3]

    // 値を追加する1:  追加後のLengthがCapacityを超えない場合
    s1 = append(s1, 6)
    printArray(originalArray)   // Length: 5 Value: [0 1 2 3 6]
    printSlice(s1)                      // Length: 5 Capacity: 5 Value: [0 1 2 3 6]

    // 値を追加する2: 追加後のLengthがCapacityを超える場合
    s1 = append(s1, 7)
    printArray(originalArray)   // Length: 5 Value: [0 1 2 3 6]
    printSlice(s1)                      // Length: 6 Capacity: 10 Value: [0 1 2 3 6 7] 
}
~省略~

値を追加する2 の後、元の配列 (originalArray) には 7 が追加されていません。

そのため、メソッドに Slice を渡して値を変更 -> 戻す というようなことがしたい場合には注意が必要そうです。

まぁそんな方々で同じ配列を変更しようとするな、ということかもしれませんが。

おわりに

Slice は自動で Length や Capacity をうまく設定してくれるなど便利な面も多いですが、
どういう動きになるのかをちゃんと理解していないと容易にバグも引き起こしそう、ということで怖さもあるなぁ、と感じました。
(こなみかん)

参照