15. スライスのハマりどころ
- 15.1 スライス式でスライスからスライスを作る
- 15.2 ハマりやすいキャパシティのおはなし
- 15.3 copyでスライスからスライスを作る
ポイントは以下の2つです。
- スライス式を使ってスライスからスライスを作る
- 2つのスライスは同じ配列を参照している。
- 一方のスライスの値を変更して、もう一方のスライスの値が変更されるかどうかはキャパシティに依存する。
- copy を使ってスライスの値をスライスにコピーする
- 2つのスライスは同じ配列を参照しない。
- 一方のスライスの値を変更しても、もう一方のスライスの値は変更されない。
15.1 スライス式でスライスからスライスを作る
スライス式を使うことで、スライスからスライスをつくることが出来ます。
例として次のようなコードを書きました。
// スライスsを定義 s := []int{1, 2, 3, 4, 5} fmt.Printf("s:%d\n", s) // "s:[1 2 3 4 5]"と出力される // スライス式を使ってsからスライスs1を定義 s1 := s[3:5] fmt.Printf("s1:%d\n", s1) // "s1:[4 5]"と出力される
このとき、スライス s とスライス s1 は次の図のような関係です。
s1 は s を参照しているわけではなく、s が参照している配列を参照します。
次にs1の値を変更します。
// スライスsを定義 s := []int{1, 2, 3, 4, 5} fmt.Printf("s:%d\n", s) // "s:[1 2 3 4 5]"と出力される // スライス式を使ってsからスライスs1を定義 s1 := s[3:5] fmt.Printf("s1:%d\n", s1) //"s1:[4 5]"と出力される // s1のインデックス0の値を変更 fmt.Println("--s1のインデックス0の値を変更--") s1[0] = 4000 fmt.Printf("s:%d\n", s) // "s:[1 2 3 4000 5]"と出力される fmt.Printf("s1:%d\n", s1) //"s1:[4000 5]"と出力される
s1の値が変更されると、sの値も変更されました。
このとき、スライスsとスライスs1は次の図のような関係になります。
s1の値を変更すると、s1が参照している配列(=sが参照している配列)の値が変更されるため、sの値も変更されます。
15.2 ハマりやすいキャパシティのおはなし
不思議(?)なスライスの挙動
次のようなスライスを使ったコードを書きました。
スライス式でスライス s から スライス s1 を作成し、値の変更や要素の追加をすると、s や s1 の値はそれぞれどうなるでしょうか。
//-------------------------------- // STEP 1: スライス s を定義 //-------------------------------- s := []int{1, 2, 3, 4, 5, 6} fmt.Printf("s:%d\n", s) // "s:[1 2 3 4 5 6]" //-------------------------------- // STEP 2: スライス式を使ってsからスライスs1を定義 //-------------------------------- s1 := s[2:5] fmt.Printf("s1:%d\n", s1) // "s1:[3 4 5]" fmt.Printf("s1の長さ:%d\n", len(s1)) // "s1の長さ:3" fmt.Printf("s1のキャパシティ:%d\n", cap(s1)) // "s1のキャパシティ:4" //-------------------------------- // STEP 3: s[2] の値を変更する // → s1 の値も変わる //-------------------------------- fmt.Println("--s[2] の値を変更--") s[2] = 300 fmt.Printf("s:%d\n", s) // "s:[1 2 300 4 5 6]" fmt.Printf("s1:%d\n", s1) // "s1:[300 4 5]" //-------------------------------- // STEP 4: s1[1] の値を変更する // → s の値も変わる //-------------------------------- fmt.Println("--s1[1]の値を変更--") s1[1] = 400 fmt.Printf("s:%d\n", s) // "s:[1 2 300 400 5 6]" fmt.Printf("s1:%d\n", s1) // "s1:[300 400 5]" //-------------------------------- // STEP 5: s1に要素"10"を追加する // → s1 の長さが 3 → 4 に変わる。 // → s1[3] に該当する s[4] の値が変わる。 //-------------------------------- fmt.Println("--s1に要素 10 を追加--") s1 = append(s1, 10) fmt.Printf("s1の長さ:%d\n", len(s1)) // "s1の長さ:4" fmt.Printf("s1のキャパシティ:%d\n", cap(s1)) // "s1のキャパシティ:4" fmt.Printf("s:%d\n", s) // "s:[1 2 300 400 5 10]" fmt.Printf("s1:%d\n", s1) // "s1:[300 400 5 10]" //-------------------------------- // STEP 6: s1に要素"20"を追加する // → s1 の長さが 4 → 5、キャパシティが 4 → 8 に変わる。 // → s1[4] に該当する s[5] が追加されない。 //-------------------------------- fmt.Println("--s1に要素 20 を追加--") s1 = append(s1, 20) fmt.Printf("s1の長さ:%d\n", len(s1)) // "s1の長さ:5" fmt.Printf("s1のキャパシティ:%d\n", cap(s1)) // "s1のキャパシティ:8" fmt.Printf("s:%d\n", s) // "s:[1 2 300 400 5 10]" fmt.Printf("s1:%d\n", s1) // "s1:[300 400 5 10 20]" //-------------------------------- // STEP 7: s[2] の値を変更する // → s[2] に該当する s1[0] の値が変わらない。 //-------------------------------- fmt.Println("--s1[2]の値を変更--") s[2] = 30000 fmt.Printf("s:%d\n", s) // "s:[1 2 30000 400 5 10]" fmt.Printf("s1:%d\n", s1) // "s1:[300 400 5 10 20]" //-------------------------------- // STEP 8: s1[1] の値を変更する // → s1[1] に該当する s[3] の値が変わらない。 //-------------------------------- fmt.Println("--s1[1]の値を変更--") s1[1] = 40000 fmt.Printf("s:%d\n", s) // "s:[1 2 30000 400 5 10]" fmt.Printf("s1:%d\n", s1) // "s1:[300 40000 5 10 20]"
sとs1の変更が、互いに影響するときとと影響しないときがありました。違いは何でしょうか。
不思議(?)なスライスの挙動の答え
ポイントはスライスの長さとキャパシティの関係です。
キャパシティは、スライスの開始から参照する配列の終わりまでの要素数です。
例として挙げたコードでは、s1が参照している配列の領域は、キャパシティ”4”で指定されている3-6の範囲です。
このとき、スライスsとスライスs1は次の図のような関係です。
始めにsとs1の要素を変更したとき、互いに影響を受けて値が変更されました。
このとき、スライスsとスライスs1は次の図のような関係です。
上記の「スライスからスライスを作る」で例に挙げたときと同じように、sとs1が参照しているの配列が変更されました。
このとき、s1のキャパシティは余裕がある状態でした。
次に、s1に要素が追加されると、sの値が変更されました。
このとき、スライスsとスライスs1は次の図のような関係です。
appendはスライスのキャパシティに余裕があると、スライスが参照している配列の値を変更します。
s1の追加箇所に該当する配列の値が変更されたため、同じ配列を参照しているsの配列も変更されたのです。
この状態で、さらにs1に要素を追加すると、s1の長さは5になり、キャパシティが8になりました。
また、s1の要素の追加は、sには反映されませんでした。
このとき、スライスsとスライスs1は次の図のような関係です。
スライスはキャパシティを超えて要素が追加されると、参照していた配列とは別に新たな配列を作り、それを参照するようになります。
s1に要素が追加されたとき、s1が参照している配列と、sが参照している配列が異なっていたので、
s1の値が追加されても、sの値は変更されなかったのです。
このときにsとs1の値を変更しても、互いに影響がなかったのも同じ理由です。
長いコードを書く上で、キャパシティを気にしながらスライスの値を変更するのは面倒なので、スライスs1の値を変更してもスライスsに影響しない方法を紹介します。
15.3 copyでスライスからスライスを作る
Goにはスライスの要素をコピーするための、組み込み関数 copy があります。
copy(コピー先のスライス, コピー元のスライス)
copyを使うときは、あらかじめmakeなどでコピー先のスライスを作っておく必要があります。
また、copyはコピーした要素数を返します。
// スライスsを定義 s := []int{1, 2, 3, 4, 5, 6} fmt.Printf("s:%d\n", s) // “s:[1 2 3 4 5 6]”が出力される // 長さ3、キャパシティ6のスライスs1を定義 s1 := make([]int, 3, 6) // copy組み込み関数を使って、sの値をs1にコピー c := copy(s1, s[2:5]) fmt.Printf("s1:%d\n", s1) // “s1:[3 4 5]”が出力される fmt.Printf("コピーした要素の数:%d\n", c) // “コピーした要素の数:3”が出力される // s1の値を変更 fmt.Println("--s1の値を変更--") s1[0] = 1000 fmt.Printf("s:%d\n", s) // “s:[1 2 3 4 5 6]”が出力される fmt.Printf("s1:%d\n", s1) // “s1:[1000 4 5]”が出力される
s1はmake組み込み関数を使って作られているため、sと参照している配列が異なります。
そのため、s1の値を変更しても、sの値には影響されません。
ただ、copy組み込み関数を使うときは注意があります。
copyはコピー先のスライスの長さの分しかコピーできません。
例として次のようなコードを書きました。
// スライスsを定義 s := []int{1, 2, 3, 4, 5, 6} fmt.Printf("s:%d\n", s) // “s:[1 2 3 4 5 6]”が出力される // 長さ2、キャパシティ6のスライスs1を定義 s1 := make([]int, 2, 6) // copy組み込み関数を使って、sの値をs1にコピー c := copy(s1, s[2:5]) fmt.Printf("s1:%d\n", s1) // “s1:[3 4]”が出力される fmt.Printf("コピーした要素の数:%d\n", c) // “コピーした要素の数:2”が出力される
sの要素を3つコピーしようとしましたが、s1の長さが2しかなかったので、要素は2つしかコピーされませんでした。
copy組み込み関数を使うときは、コピー先のスライスの長さに注意してください。