技術向上

プログラミングの学び、気になるテクノロジーやビジネストレンドを発信

スライス【Go】

スライス

スライスは配列と異なり、宣言時のサイズを持ちません。
あくまで「配列の参照」です(配列はサイズも含めて型情報となります)。

a := []int{1, 3}
b := a
b[0] = 3
fmt.Println(a)    // [3 3]
fmt.Println(b)    // [3 3]

a、b共に同じ配列の実体を参照しています。

反対にスライスではなく配列の場合、それぞれは別の配列です。
メモリを確認してみるとその事がハッキリとわかります。
&はアドレス演算子と呼ばれるもので、変数のアドレスを取得するための演算子です。

a := [2]int{1,3}
b := a
b[0] = 3

fmt.Println(a)    // [1 3]
fmt.Println(b)    // [3 3]

fmt.Println(&a[0])     // 0xc000078070
fmt.Println(&b[0])    // 0xc000078080

注意が必要なのは、配列の場合、
コピーされた別の配列が生成されるため、
呼び出し元で引数を渡しても、関数の中では別の配列が操作されます。
これを値渡しと言います。
よって、期待した出力結果を得ることはできません。

func main() {
  s := [3]int{1, 2, 3}
  takeSlice(s)
  fmt.Println(s)    // [1 2 3]
}

func takeSlice(s [3]int) {
  s[1] = 0    // 機能しない
}

これはポインタ渡しにすることで解決できます。

func main() {
  s := [3]int{1, 2, 3}
  takeSlice(&s)
  fmt.Println(s)    // [1 0 3]
}

func takeSlice(s *[3]int) {
  s[1] = 0
}

ポインタ渡しとは、メモリ上のアドレスを指すポインタを別のアドレスに渡すことです。
ポインタで変更したい箇所を指定できるので、意図通りに対象を変更できます。
*は関接演算子で、アドレスから変数の値を取得する演算子です。

対してスライスは参照渡しです。
参照渡しは、スライスそれ自体は呼び出し元と先で異なるアドレスを持ちますが、
中の要素は呼び出し元と先が同じアドレスですので、期待通りの結果を得る事ができます。

func main() {
    s := []int{1, 2, 3}
    takeSlice(s)
    fmt.Println(s)    // [1 0 3]
    fmt.Printf("%p\n", &s)   // 0xc000086060 異なる
    fmt.Println(&s[0])     // 0xc000088020 中身は同じ
}

func takeSlice(s []int) {
    s[1] = 0
    fmt.Printf("%p\n", &s) // 0xc000086040 異なる
    fmt.Println(&s[0])     // 0xc000088020 中身は同じ
}

スライス自体(関数の引数となる)のアドレスが異なるのは、
Goにおける関数の引数は基本的に値渡しになるからです。

slicing

slicingは、配列やスライスから切り取って、新たなスライスを生成する操作です。
スライスですので、参照先は同じです。

a := [3]int{1, 2, 3}
b := a[1:]
fmt.Println(b)    // [2 3]

c := []int{7, 8, 9}
d := c[1:]
fmt.Println(d)    // [8 9]

: を用いて指定するのは、カンマの数です。

a := [3]int{1, 2, 3}
// もしくは a := []int{1, 2, 3}

b := a[1:3]    // 1つ目のカンマの後から3つ目のカンマ(最後)まで
c := a[1:]    // 1つ目のカンマの後から最後まで
d := a[0:2]    // 最初から2つ目のカンマまで
e := a[:2]    // 最初から2つ目のカンマまで
f := a[0:3]    // 最初から3つ目のカンマ(最後)まで
g := a[:]    // 最初から最後まで

参照先は同じ配列であり、どこを切り取るかがslicingですので、
代入された参照範囲を変更することもできます。

a := [3]int{1, 2, 3}
b := a[0:1]
fmt.Println(b) // [1]
b = b[0:3]
fmt.Println(b) // [1 2 3]

ただし、開始位置については、最初に切り取った範囲内に収める必要があります。

append

スライスは後から要素を追加する事ができます。
このように、プログラムの実行中にメモリ領域を割り当てることを「メモリの動的割り当て」と言います。

a := []int{1}
fmt.Println(&a[0])    // 0xc000082008 異なる
a = append(a, 2)
fmt.Println(&a[0])    // 0xc000082020 異なる

appendは対象のスライスの容量(capacity)をオーバーした時に、新たに配列を生成します(allocate)。
allocateはメモリを消費しますので、事前に要素数がわかる場合はmake関数を使うべきです。

a := make([]int, 2, 3)    // 第2引数はlength、第3引数がcapacity
a[0] = 1
a[1] = 2
fmt.Println(&a[0])    // 0xc000086020
a = append(a, 3)
fmt.Println(&a[0])    // 0xc000086020

allocateが発生するのはmake関数を使用した直後だけです。
append直後もcapacity以内なので、allocateは発生しません。

ここで注意点があります。
make関数の第2引数lengthがありますが、
append(メモリの動的割り当て)をする前の長さより大きく指定してしまうと、
その時点で0埋めされてしまうため、
appendした時に気づかず、capacityをオーバーしてallocateを引き起こしてしまう恐れがあります。

実はmake関数では、第3引数を省略できるのですが、
省略すると、第2引数がlengthとcapacityを現すことになり、
appendをすると、必然的にallocateが発生してしまいます。
appendする場合には、第3引数を省略しないようにします

さらに、別変数に格納するわけでもない限り、
appendを用いないほうが、メモリ効率は良くなります。

a = append(a, 3)

上記のような、同一変数に対してappendした結果を代入する方式は、
その変数を一度破壊して代入するので、余計なメモリを消費するためです。

a := make([]int, 5)
for i := 0; i < 5; i++ {
  a[i] = i
}

appendではなく、スライスの順番を指定して置き換えるので、
動的なメモリ割り当ては発生しません。
したがってmake関数の第3引数は省略できます。
ただし、capacity(第2引数)を超えるような対象に代入をしないように注意します。

ちなみに、capacityをオーバーした時の容量の増え方ですが、
その時点における、capacityの2倍の値に増加するようになっています。
これは、次の要素数の増加に効率的に備えるための工夫です。

また、make関数を用いないときのcapacityの初期値は、
宣言時の要素数になります。


なお、Goの場合、ガベージコレクション機能があるので、
どの変数からも使用されなくなったメモリ領域は、自動的に回収されて再利用されます。

スライスの連結

appendを用いて、第1引数と第2引数を連結します。
第2引数の後に、「...」を記述する必要があります。

b := []int{
 100, 200, 300,
}

c := make([]int, 1, cap(b)+1)    // allocateが発生しないことを確認するため、初期lenを1とし、初期capはcap(b)にその1を加算
fmt.Println(c)    // [0]
fmt.Println(&c[0])    // 0xc000096020
c = append(c, b...)
fmt.Println(c)    // [0 100 200 300]
fmt.Println(&c[0])    // 0xc000096020


特定要素の削除

func del(a []int, i int) []int {
    var zero int    // 宣言のみ行うことでゼロ値を生成
    copy(a[i:], a[i+1:])    // この時点でaは[1 2 4 5 5]
    a[len(a)-1] = zero    // 不要な末尾にゼロ値を代入
    a = a[:len(a)-1]   // 不要な末尾を含めない
    return a
}

func main() {
    a := []int{1, 2, 3, 4, 5}
    a = del(a, 2)
    fmt.Println(a, len(a), cap(a))   // [1 2 4 5] 4 5
}

copy関数を使って、要素を詰めます。

copy(コピー先, コピー元)

正確には、コピー先の方が長い場合には、
コピー先の超過分が末尾が残される形になります。
その不要な要素にゼロ値を代入することによって、
確実にガベージコレクション(不要になったメモリ領域を再利用)される状態にできます。

ゼロ値を代入しなくても、allocateは発生しませんし、スライスからも削除されるのですが、
内部的に保持する配列には残存してしまう事があるようです。
その事によってガベージコレクションが行われない可能性もあるため、確実な方法を取るべきです。

なお、ゼロ値はそのスライスの型と同じ型である必要あります。
stringの場合は次のようにします。

func delS(b []string, i int) []string {
    var zero string    // stringで宣言
    copy(b[i:], b[i+1:])
    b[len(b)-1] = zero
    b = b[:len(b)-1]
    return b
}

func main() {
    b := []string{"a", "b", "c", "d", "e"}
    b = delS(b, 2)
    fmt.Println(b, len(b), cap(b))
    fmt.Println(b[len(b)-1])
}


qiita.com

qiita.com

qiita.com

qiita.com

christina04.hatenablog.com

jxck.hatenablog.com