スライスとは
可変長配列を持たない代わりに実装された型。
配列全体のポインタ(ptr)、配列の長さ(len)、配列の容量(cap)からなるデータ構造を保持している。配列の部分列を簡単に取り出せる。
Go言語では、配列よりもスライスのほうが多く使われるようだ。
実際、固定長よりも可変長で配列扱いたい感あるしね。
型の宣言
var hoge []int
var hoge = []int{1, 2, 3}
hoge := []int{1, 2, 3}
[3]
とか[...]
せずに[]
とする。要素数は書かない。
こんなもんかな
動作の確認
スライスという名前の通り、予め用意した配列を要素を部分列として取り出すことができる。
package main
import (
"fmt"
)
func main() {
a := [5]string{"Haruka", "Chihaya", "Miki", "Yukiho", "Makoto"}
b := a[:]
c := a[2:4]
// d := a[2:6] // invalid slice index 6 (out of bounds for 5-element array)
fmt.Println(a) // [Haruka Chihaya Miki Yukiho Makoto]
fmt.Println(b) // [Haruka Chihaya Miki Yukiho Makoto]
fmt.Println(c) // [Miki, Yukiho]
b[1] = "Hibiki"
c[0] = "Yayoi"
fmt.Println(a) // [Haruka Hibiki Yayoi Yukiho Makoto]
fmt.Println(b) // [Haruka Hibiki Yayoi Yukiho Makoto]
fmt.Println(c) // [Yayoi Yukiho]
b := a[:]
やc := a[2:4]
から分かる通り、配列の要素を指定しスライスに代入している。範囲外はもちろんコンパイルエラー。取り出し方は以下のとおり
取り出し方 | 解説 |
---|---|
a[:] | 全要素 |
a[2:] | 2番目から最後まで |
a[2:5] | 2番目から(5-1)番目まで |
a[:5] | 初めから(5-1)番目まで |
はじめからとか最後までと指定するときは省略できる。
スライスに渡した部分列の要素は元の配列の メモリ領域が共有 される。なので、スライス型で要素を代入するということは、元の配列データに代入ということになる。
ちなみに、組み込み関数 len()
、cap()
で指定したスライスのlenとcapのデータを参照できる。
関数で扱う場合
package main
import (
"fmt"
)
func change1(a []int) {
a[2] += 100
}
func change2(b []int) {
b[2] += 200
}
func main() {
a := []int{1, 2, 3, 4, 5}
change1(a)
fmt.Println(a) // [1 2 103 4 5]
change2(a)
fmt.Println(a) // [1 2 303 4 5]
}
メモリ共有がなされているので関数内で操作しても、元のスライスも変更される。
引数として渡してるのは配列の要素ではなく、配列全体のメモリアドレスっぽい。
組み込み関数 make() & append()
スライスでは、組み込み関数として、make()
とappend()
を使うことができる。
make関数
// make()
package main
import (
"fmt"
)
func main() {
var a []int
a = make([]int, 5, 10)
fmt.Println(a) // [0 0 0 0 0]
fmt.Println(len(a)) // 5
fmt.Println(cap(a)) // 10
for i, _ := range a {
a[i] = i
}
fmt.Println(a) // [0 1 2 3 4]
b := make([]int, 10)
fmt.Println(b) // [0 0 0 0 0 0 0 0 0 0]
fmt.Println(len(b)) // 10
fmt.Println(cap(b)) // 10
}
make(型, 要素数, 容量)
として使う。
スライスが初期化され、デフォルトではゼロ値として0
が格納されている。
容量を省略すると、要素数==容量
となる。
append関数
なかなかに癖のある関数
package main
import (
"fmt"
)
func main() {
a := []string{"Haruka", "Chihaya", "Miki", "Yayoi", "Iori"}
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori]
fmt.Println(len(a)) // 5
fmt.Println(cap(a)) // 5
a = append(a, "Hibiki", "Takane")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Hibiki Takane]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 10
a = append(a, "Makoto", "Yukiho", "Ritsuko", "Azusa")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Hibiki Takane Makoto Yukiho Ritsuko Azusa]
fmt.Println(len(a)) // 11
fmt.Println(cap(a)) // 20
}
append(スライス, データ1, データ2, ...)
として使う。
指定したスライスの末尾に追加していく。
スライスの要素数は自動で増えていく。
容量はいっぱいになった時、 現在の容量を倍 にして確保してくれる。
この時、配列のメモリアドレスは容量の拡張に伴い変更される。
面白い例をみせましょう。
// 十分な容量を確保している場合
package main
import (
"fmt"
)
func main() {
a := make([]string, 7, 10) // 要素数 < 容量
a[0] = "Haruka"
a[1] = "Chihaya"
a[2] = "Miki"
a[3] = "Yayoi"
a[4] = "Iori"
a[5] = "Hibiki"
a[6] = "Takane"
b := append(a[:5],"Ami", "Mami")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 10
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(b)) // 7
fmt.Println(cap(b)) // 10
b = append(a[:5],"Hibiki", "Takane", "Ami", "Mami")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Hibiki Takane]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 10
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Hibiki Takane Ami Mami]
fmt.Println(len(b)) // 9
fmt.Println(cap(b)) // 10
b[5] = "Ritsuko"
b[6] = "Azusa"
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Ritsuko Azusa]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 10
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Ritsuko Azusa Ami Mami]
fmt.Println(len(b)) // 9
fmt.Println(cap(b)) // 10
}
b := append(a[:5],"Ami", "Mami")
b = append(a[:5],"Hibiki", "Takane", "Ami", "Mami")
スライスbにaの5番目までのデータ+"Ami", "Mami"をappendする。
配列のメモリを共有しているので、aとbは同じデータ構造となる。
しかし、aの要素数を越えてappendするとそれぞれの出力結果は異なるが、容量に変化はないため確保されている要素では共有状態にある。
// 十分な容量を確保していない場合
package main
import (
"fmt"
)
func main() {
a := make([]string, 7) // 要素数 == 容量
a[0] = "Haruka"
a[1] = "Chihaya"
a[2] = "Miki"
a[3] = "Yayoi"
a[4] = "Iori"
a[5] = "Hibiki"
a[6] = "Takane"
b := append(a[:5],"Ami", "Mami")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 7
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(b)) // 7
fmt.Println(cap(b)) // 7
b = append(a[:5],"Hibiki", "Takane", "Ami", "Mami")
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 7
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Hibiki Takane Ami Mami]
fmt.Println(len(b)) // 9
fmt.Println(cap(b)) // 14
b[5] = "Ritsuko"
b[6] = "Azusa"
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori Ami Mami]
fmt.Println(len(a)) // 7
fmt.Println(cap(a)) // 7
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori Ritsuko Azusa Ami Mami]
fmt.Println(len(b)) // 9
fmt.Println(cap(b)) // 14
}
十分な容量を確保せずにappendした場合、自動的に容量するので前述した通り配列のメモリアドレスが変更される。
こうなると、aとbはそれぞれ独立されたスライスとなってしまい、データの編集・追加をしてもそれらは共有されることはない。
なので、共有状態を保ったままデータの操作を行うときは、予め多めに容量をとっておく必要がありそう。
copy関数
スライスの要素をコピーできる
package main
import (
"fmt"
)
func main() {
a := []string{"Haruka", "Chihaya", "Miki", "Yayoi", "Iori"}
b := make([]string, 5, 10)
cLen := copy(b, a)
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori]
fmt.Println(b) // [Haruka Chihaya Miki Yayoi Iori]
fmt.Println(cLen) // 5
b[0] = "Ritsuko"
fmt.Println(a) // [Haruka Chihaya Miki Yayoi Iori]
fmt.Println(b) // [Ritsuko Chihaya Miki Yayoi Iori]
}
copy(コピー先, コピー元)
として扱う。
上記サンプルコードでは、スライスbにスライスaのデータをコピーするといった感じ。
copy関数ではデータそのものをコピーしているため、共有関係はない。
雑感
- 配列よりも便利感ある
- delete関数がないので、append関数であれこれしなきゃいけないのは面倒