tocsatoの備忘録

ほぼほぼ 50 代のプログラマの備忘録。swift golang javascript css html5 nginx mysql などを最近使ってます。

スライス操作のおさらい - 1

スライスとは

golang は配列とは別にスライスという型があります。
配列は物理的にメモリに配置される実体ですが、スライスは配列の参照に近いものです。
スライスは、長さとは別に容量を持っていることで、柔軟にかつ、効率よくメモリ操作ができます。
スライスを使いこなすことで、メモリ効率よく処理することができます。

配列の宣言

var a [2]int

a = [2]int{0, 1}   // 長さ 2 の配列
a = [...]int {0, 1} // 長さを省略した宣言方法

スライスの宣言

var b []int

b = []int{0, 1} // 長さを指定しないとスライス

b = make([]int, 2)     // 長さ, 容量 共に 2 のスライス
b = make([]int, 2, 16) // 長さ 2, 容量 16 のスライス

スライス型のオブジェクト

len(b) ... スライス b の長さ
cap(b) ... スライス b の容量

長さ, 容量が 0 の2つのスライス

長さ 0, 容量 0 のスライス

b = make([]int, 0)
b == nil ... false
len(b) ... 0
cap(b) ... 0
b = b[:0] ... ok
b == nil ... false

もうひとつの長さ 0, 容量 0 のスライス: nil

b = nil
b == nil ... true
len(b) ... 0
cap(b) ... 0
b = b[:0] ... ok
b == nil ... true

これはデータベースでいう null と空文字列に近い関係ですが、長さ 0 のスライスとして扱えるため以降の処理をスマートに記述することができます。

b = nil
sum := 0
for _, x := range b {
    sum += x
}

特にスライス b が nil であるかを判定しなくても、実行時エラーはでません。

配列を参照するスライス

配列を参照するスライスの動作を確認します。
いろいろ興味深い挙動が見えてきます。

var v [4]int
var b []int

v = [...]int{100, 101, 102, 103}
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [100 101 102 103] b = [] len=0 cap=0

スライス b = nil なので、長さ、容量は 0 です。

// 配列の一部をスライスで参照する
b = v[2:]
b[0] = 200
b[1] = 201
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [100 101 200 201] b = [200 201] len=2 cap=2

スライス b は 配列 v の一部[2:4]を参照しています。
b[0], b[1] を書き換えると、それぞれ実体の v[2], v[3] が書き換わります。

// 配列を指す長さ 0 のスライスを作る
b = v[:0]
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [100 101 200 201] b = [] len=0 cap=4

興味深いのは cap(容量) = 4 となっている点です。
長さは 0 ですが、後ろに空きがあることを示しています。
つまり、この b に対して append() してもメモリ移動は生じません。

// append() で配列 v を書き換える
b = append(b, 300, 301)
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [300 301 200 201] b = [300 301] len=2 cap=4

append() の結果、v[0], v[1] が書き換えられたことが確認できます。

// もう2つ append() する
b = append(b, 400, 401)
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [300 301 400 401] b = [300 301 400 401] len=4 cap=4

v[2], v[3] が書き換えられました。
そして、v を参照している b の len と cap が同じ 4 になっています。
ここで、更に append() すると v や b はどうなるか確認します。

// さらに append() する
b = append(b, 500, 501)
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [300 301 400 401] b = [300 301 400 401 500 501] len=6 cap=8

v に影響はなく、b だけが拡張されました。

スライスの拡張

スライスを拡張するとき、先程の例では、

  1. スライス b は初めて配列 v の参照をやめ、
  2. 新たに確保した暗黙の配列に、元の配列の内容をコピーし、
  3. スライス b は暗黙の配列を参照する

という動作を行います。

ちなみにスライスが拡張されるとき、容量が x2 づつ拡張されていきます。(少なくとも Go 1.7 までは)
スライスを操作するとき、

拡張を起きにくくする = 計算された容量のスライスを扱う

がメモリ効率のよいアプリケーションを書く第一歩といえます。

スライスの縮小

// スライスを空にする
b = b[:0]
fmt.Printf("v = %v b = %v len=%d cap=%d\n", v, b, len(b), cap(b))
v = [300 301 400 401] b = [] len=0 cap=8

スライス b から長さ 0 のスライスを作りました。
同じ暗黙の配列を参照していますので、容量は 8 のままです。
これにより、一度確保した配列の使いまわしができます。

もし、昔の暗黙の配列を破棄したいなら

b = nil

とすれば、確保していた暗黙の配列への参照がなくなるため、この配列は破棄されます。

まとめ

スライスの超基礎を改めて記してみました。
このあと、スライスを効果的に使う方法を書いてみたいと思います。