2013年1月27日日曜日

Go言語 スライスのappendについて


こんにちは。scarvizです。

今回はスライスのappendについて注意したい点を取り上げます。




■スライスのappendの動き
初期化済みのスライスに新たに要素を追加したい、スライスとスライスを連結させたい、という場合があると思います。
そういう時にappendを使用します。
以下のコードで、最初に出力されるのは要素の追加、次に出力されるのはスライスの連結です。
スライスにスライスを連結させる場合、連結させるスライスの後に「...」をつけることを忘れないようにしてください。

package main

import "fmt"

func main() {
 // スライス初期化
 intSlice := []int{1, 2, 3}
 // 新たに"4"という値の要素を加える
 intSlice = append(intSlice, 4)
 fmt.Println(intSlice)

 intSlice2 := []int{5, 6, 7}
 // スライスとスライスを連結する
 intSlice = append(intSlice, intSlice2...)
 fmt.Println(intSlice)
}

結果:
[1 2 3 4]
[1 2 3 4 5 6 7]


■ループ中にスライスにappendさせる
ループ処理の中で算出される値を、スライスに追加していくような処理を書きたいという場合があると思います。
まずは以下のコードをご覧ください。

package main

import "fmt"

func main() {
 // 長さ(キャパシティ)が10のスライス
 intSlice := make([]int, 10)
 // ループで0~9の値の要素を順番に追加
 for i := 0; i < 10; i++ {
  intSlice = append(intSlice, i)
 }
 fmt.Println(intSlice)
}

このスライスは長さ(キャパシティ)が10あり、10個int型の値を追加しようとしているものです。
これを実行すると結果は以下になります。

結果:
[0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]

これは全然期待していたものとは違うと思います。

■原因
スライスの初期化では初期値として、その型のゼロ値が格納されます。今回の場合だとint型のゼロ値(0)が10個入った状態になります。
そのため、このスライスにappendすると、0が10個の後に0~9が追加される事になります。
先ほどのコードの初期化後の状態を出力して確認してみます。

package main

import "fmt"

func main() {
 // 長さ(キャパシティ)が10のスライス
 intSlice := make([]int, 10)
 // スライスの初期状態
 fmt.Println(intSlice)
 
 // ループで0~9の値の要素を順番に追加
 for i := 0; i < 10; i++ {
  intSlice = append(intSlice, i)
 }
 fmt.Println(intSlice)
}

結果:
[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]

0が10個入ってますね。

■対策
このゼロ値を回避するには、以下のようにすれば良いです。

package main

import "fmt"

func main() {
 // 初期化時に長さ(キャパシティ)を0とする
 intSlice := make([]int, 0)
 for i := 0; i < 10; i++ {
  intSlice = append(intSlice, i)
 }
 fmt.Println(intSlice)
}

結果:
[0 1 2 3 4 5 6 7 8 9]

期待している結果になりましたね。
ちなみにスライスを初期化しない(nilの状態)ではappendできません。


気付けば大したことは無いのですが、よくありがちなミス(少なくとも僕はやっちゃってました)なので、気をつけてくださいね。

6 件のコメント:

  1. append(slice1, slice2...) の"..."は
    正確には何の意味があるんでしょうか?
    気になります!

    返信削除
    返信
    1. 僕も気になったので、ちょっと調べてみました。
      結論としては、ちゃんと分かりませんでした・・・。
      調査した結果だけ。

      実処理ではなく、ドキュメント用に用意されている
      $GOROOT/src/pkg/builtin/builtin.go
      では、appendは、
      func append(slice []Type, elems ...Type) []Type
      と定義されています。
      elemsは可変長パラメータになっているため、要素を渡すと、その要素のスライスを作成するそうです。
      ここで追加要素ではなくスライスを足したい時に、もし"..."という記号(?)がなければ、
      スライスのスライスを作成することになります。
      なので、足し合わせるスライスには明示的に"..."を付けているのではないかなと思います。

      ただ、追加要素なのかスライス自体なのかは、スライスの場合に"..."を付けないとエラーになるので、
      見分けれるんじゃないかなとも思うので、実際のところは分かりません。
      分かる方いたら教えてください!

      削除
  2. 大きさが分かっている場合は、intSlice := make([]int, 0,10)のようにして、キャパシティ(長さではない)を指定してやるといいようですよ。appendした際にバックにある配列が再生成されないようです。
    http://golang.org/pkg/builtin/#append

    返信削除
    返信
    1. ありがとうございます!
      なるほど。最初説明を読んだときは、引数に渡したスライスが追加されて返って来るのかと思ってましたが、内部的に再作成するかどうかを判断してるということなんですね。
      そもそも、スライスを引数で渡しても参照渡しではないので、それに追加されて返ってくる事は無いですね。

      削除
  3. http://golang.org/ref/spec#Calls
    にちゃんと説明ありました。
    ...Type: でType指定してますから
    なんでもかんでも渡す、というものではないようなので、
    コンパイル時にちゃんとちぇっく入りそうですね。

    append固有の話は調べてませんが...

    返信削除
    返信
    1. 英語半分しか読んでない(読めてない)ですが、それっぽい事書いてますね。
      appendの定義も同じなので、というか、"..."についてはどれも同じだと思うので、理由としては合ってそうですね。
      見分けてくれたら楽なんですけど(笑)

      削除