演習 - スライスについて調査する

完了

前のセクションで、配列について説明し、配列がスライスとマップの基盤となっていることを学びました。 その理由については、すぐに理解できるでしょう。 配列と同様に、スライスは、同じ型の要素が連続していることを表す Go のデータ型です。 ただし、配列との大きな違いは、スライスのサイズは固定ではなく動的であるということです。

スライスとは、配列または別のスライスの上に作成されるデータ構造です。 元の配列またはスライスを "基になる配列" と呼びます。 スライスを使用すると、基になる配列全体または要素の部分列のみにアクセスできます。

スライスのコンポーネントは次の 3 つのみです。

  • 基になる配列の最初の到達可能な要素へのポインター。 この要素は、必ずしも配列の最初の要素 array[0] であるとは限りません。
  • スライスの長さ。 スライス内の要素数。
  • スライスの容量。 スライスの始めから、基になる配列の終わりまでの要素数。

次の図は、スライスとはどのようなものであるかを示しています。

Diagram showing how slices look in Go.

スライスは基になる配列のサブセットにすぎないことに注目してください。 前の図をコードで表現する方法を見てみましょう。

スライスを宣言して初期化する

スライスを宣言するには、配列を宣言するときと同じ方法で宣言します。 たとえば、次のコードはスライスの図に表示されていた内容を表しています。

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

このコードを実行すると、次の出力が表示されます。

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

現時点では、スライスは配列とそれほど異なっていないことに注目してください。 それらは同じ方法で宣言します。 スライスから情報を取得するには、組み込み関数 len() および cap() を使用できます。 これらの関数を引き続き使用して、スライスは、基になる配列の要素の部分列を持つことができることを確認します。

項目のスライスを作成する

Go では、スライス演算子 s[i:p] がサポートされています。ここで、

  • s は配列を表します。
  • i は、新しいスライスに追加する、基になる配列 (または別のスライス) の最初の要素へのポインターを表します。 変数 i は、配列 array[i] 内のインデックス位置 i にある要素に対応します。 この要素は、必ずしも基になる配列の最初の要素 array[0] ではないことにご注意ください。
  • p は、新しいスライスを作成するときに使用する、基になる配列内の要素数と要素の位置を表します。 変数 p は、新しいスライスで使用できる、基になる配列の最後の要素に対応します。 基になる配列内の位置 p にある要素は、位置 array[i+1] で見つかります。 この要素は、必ずしも基になる配列の最後の要素 array[len(array)-1] ではないことに注目してください。

したがって、スライスは要素のサブセットのみを参照できます。

4 つの変数が年の各四半期を表し、スライス months に 12 個の要素があるとします。 次の図は、months を 4 つの新しい quarter にスライスする方法を示しています。

Diagram showing how multiple slices look in Go.

前の図に示されていた内容をコードで表すには、次のコードを使用できます。

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

このコードを実行すると、次の出力が表示されます。

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

スライスの長さは同じですが、容量が異なることに注目してください。 quarter2 のスライスを見てみましょう。 このスライスを宣言するときに、スライスが位置番号 3 で始まり、最後の要素を位置番号 6 にするように指定しています。 このスライスの長さは 3 つの要素ですが、容量は 9 です。基になる配列には使用可能な要素または位置がさらにありますが、このスライスには表示されていないためです。 たとえば、fmt.Println(quarter2[3]) のように出力しようとすると、次のエラーが表示されます: panic: runtime error: index out of range [3] with length 3

スライスの容量によって示されるのは、スライスをどこまで拡張できるかです。 このため、次の例のように、quarter2 から拡張されたスライスを作成できます。

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

前述のコードを実行すると、次の出力が表示されます。

[April May June] 3 9
[April May June July] 4 9

quarter2Extended 変数を宣言するときに、最初の位置を指定する必要がない ([:4]) ことに注目してください。 このように指定すると、Go ではスライスの最初の位置が必要であると見なされます。 最後の位置 ([1:]) に対しても同じ指定を行うことができます。 Go では、スライスの最後の位置 (len()-1) までのすべての要素を参照する必要があると見なされます。

項目を追加する

スライスのしくみと、それらが配列にどのように似ているかを詳しく説明しました。 次に、配列との違いについて説明します。 最初の違いは、スライスのサイズが固定されておらず、動的であることです。 スライスを作成したら、それにさらに要素を追加することができ、それによりスライスが拡張されます。 基になる配列に何が起こるかはすぐにわかります。

要素をスライスに追加するために、Go には append(slice, element) 組み込み関数が用意されています。 変更するスライスと追加する要素を値として関数に渡します。 append 関数から返される新しいスライスを変数に格納します。 これは変更するスライスと同じ変数にすることができます。

追加プロセスがどのようになるかをコードで見てみましょう。

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

上記のコードを実行すると、次の出力が表示されます。

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

この出力は興味深いです。 特に、cap() 関数の呼び出しによって返される内容です。 3 回目の反復まではすべて通常どおりに見えます。3 回目の反復では、容量が 4 に変更され、スライス内には 3 つの要素のみがあります。 5 回目の反復では容量が再び 8 に変更され、9 回目では 16 に変更されます。

容量の出力のパターンに気づきましたか。 "それより多くの要素を保持するだけの十分な容量がスライスにない場合、Go によってその容量が 2 倍にされます"。 新しい基になる配列が、新しい容量で作成されます。 ユーザーは容量を増やすために何もする必要はありません。 Go によって自動的に実行されます。 注意する必要があります。 ある時点で、スライスの容量が必要以上に多くなり、メモリが無駄に消費されるおそれがあります。

項目を削除する

要素を削除するにはどのようにすればいいのだろうと思われているかもしれません。 Go にはスライスから要素を削除するための組み込み関数はありません。 前に説明したスライス演算子 s[i:p] 使用すると、必要な要素のみで新しいスライスを作成できます。

たとえば、次のコードではスライスから要素を削除しています。

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

前述のコードを実行すると、次の出力が表示されます。

Before [A B C D E] Remove  C
After [A B D E]

このコードではスライスから要素を削除しています。 削除する要素が、スライス内のその次の要素で置き換えられます。最後の要素を削除する場合は、何も置き換えられません。

もう 1 つの方法は、スライスの新しいコピーを作成することです。 スライスのコピーを作成する方法について、次のセクションで学習します。

スライスのコピーを作成する

Go には、スライスのコピーを作成するための組み込みの copy(dst, src []Type) 関数があります。 コピー先のスライスとソースのスライスを与えます。 たとえば、次の例に示すようにスライスのコピーを作成できます。

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

なぜコピーを作成しようとしているのでしょうか。 スライスの要素を変更すると、基になる配列も変更されます。 同じ基になる配列を参照している他のすべてのスライスが影響を受けます。 このプロセスをコードで見てから、スライスのコピーを作成して修正しましょう。

次のコードを使用して、スライスによって配列が指し示されていることを確認します。スライスで行ったすべての変更は、基になる配列に影響します。

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

上記のコードを実行すると、次の出力が表示されます。

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

slice1 に対して行った変更によって、letters 配列と slice2 が影響を受けていることに注目してください。 この出力では、文字 B が Z に置き換えられ、letters 配列を指しているすべてにそれが影響していることがわかります。

この問題を解決するには、スライスのコピーを作成する必要があります。この場合、内部では新しい基になる配列が作成されます。 次のコードを使用できます。

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

上記のコードを実行すると、次の出力が表示されます。

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

slice1 での変更は基になる配列に影響しましたが、新しい slice2 には影響しなかったことに注目してください。