Go:for 文の range の挙動で分からなかった部分を調べた
最近 Go を学び始めた。勉強の最中に for 文の range の挙動で分からない部分があったので調べてみた。
分からなかった挙動
自分が理解できなかったのは以下のコードの挙動。
package main import ( "fmt" ) func main() { ns := []int{10, 20, 30} for i, n := range ns { fmt.Printf("%d: %d\n", i, n) fmt.Printf("ptr to n : %p\n", &n) fmt.Printf("ptr to ns[i] : %p\n", &(ns[i])) } }
コードの概要としては、スライスの要素を for 文 と range 句を用いて取り出し、ループ毎に以下の 3 つを出力している。
- range で取り出したインデックスとスライスの要素(
%d: %d ...
の部分) - range で取り出したスライスの要素へのポインタ(
ptr to n ...
の部分) - スライスから直接取り出した要素へのポインタ(
ptr to ns[i] ...
の部分)
実際の出力を以下に示した。
0: 10 ptr to n : 0xc000018030 ptr to ns[i] : 0xc000016018 1: 20 ptr to n : 0xc000018030 ptr to ns[i] : 0xc000016020 2: 30 ptr to n : 0xc000018030 ptr to ns[i] : 0xc000016028
私は、「ループ毎に『range で取り出したスライスの要素へのポインタの値』が変わる」ということを期待したが、実際にはそのポインタの値は全て同じ値が出力された。上記の出力を見ると、ptr to n: ...
の ...
の部分が 3 回のループで全て同じ値が出力されているのが分かる。「ループ毎に要素が順々に変わるためその要素へのポインタの値も変わるはず...」と思っていたが違うみたい。同時に ns[i]
へのポインタの値も出力したが、こちらは期待通りにループ毎に異なったポインタの値が出力されたので、私が理解できていないのは range の挙動だと考えられる。本記事ではこれについて調べていく。
以下のリンクで上記のコードの実行結果を確認できる(ポインタの値は実行ごとに変わる可能性があるので注意)。
調査
調査していく。
range の仕様の調査
range の仕様は The Go Language Specification の "Statements" > "For statements" > "For statements with range clause" に記述されている(range clause = range 句)。
range 句の仕様は以下の通り。
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
range 句の中でスライスを使用したときの仕様は以下のようになっており、for i, n := range sl {...}
(sl
はスライスを表す)と実装したときは、1 つ目の値が 0-origin のインデックス i
、2 つ目の値がインデックスに対応したスライスの要素の値 sl[i]
となる。
Range expression 1st value 2nd value array or slice a [n]E, *[n]E, or []E index i int a[i] E
これについては自分の理解が間違っている部分はなかった。
しかし、仕様を読んでいると自分の調べている内容とドンピシャの内容が書いてあった。以下、引用。太字の部分に着目。
The iteration variables may be declared by the "range" clause using a form of short variable declaration (
:=
). In this case their types are set to the types of the respective iteration values and their scope is the block of the "for" statement; they are re-used in each iteration. If the iteration variables are declared outside the "for" statement, after execution their values will be those of the last iteration.
(引用元)
ざっくり訳すと以下の通りになる。
イテレーション変数(先に示したコードにおいて、range 句でスライスを利用して宣言した
i
、n
のこと)は:=
を利用した短い変数宣言の形式を用いて range 句で宣言される。 この場合、イテレーション変数の型は各イテレーションの値の型に設定され、これらのスコープは for 文のブロックとなる。これらは各イテレーションで再利用される。 もしイテレーション変数が for 文の外で宣言された場合、実行後の値は最後のイテレーションの値となる。
上記に自分の疑問への答えが含まれていた。
私は for i, n := range sl {...}
という for 文を実装したとき、「n
にはループ毎に sl[i]
が入るから、その値へのポインタも sl[i]
へのポインタに等しい。だからループ毎に n
へのポインタも書き換わるはず。」と考えていた。しかし、実際には n
はループ用に定義された一時的な変数であり、for 文のブロックに入った時に一度だけメモリが確保され、ループ毎に値が上書きされるだけの変数である。つまり、range によって定義された n へのポインタはループ中は一定である。これで自分の疑問は解決した。
ちなみに、自分と全く同じ状況の人を Stackoverflow で見かけた。
スライスの仕様の調査
ついでにスライスの仕様についても少し調べた。
配列やスライスなどの i 番目の要素へのアクセス a[i]
についての仕様は The Go Language Specification の "Expressions" > "Index expressions" に記述されている。
所感
調べてみたらすごく初歩的なことだったので自分の頭の悪さに少しがっかりした。range の挙動については言われて見ればまあそうだよねとなった。でもスライスを for-range で回してその要素のポインタを取るみたいなシチュエーションは割とありそうなので早めにその部分の仕様を理解できたのは良かった。
あとドキュメントを丁寧に読む習慣を付けたい。英語だと目が滑ってしまい流して読んでしまい、そこが実は重要だったみたいなことが稀によくある。
参考
- 本記事の発端となった記事。下記の記事の中で「for の中で t(イテレーション変数)のポインタは常に同じなので...」という一文がさらっと書いてあり「あれそうだったっけ?」と自分は引っかかってしまいこの記事を書くに至った。
- ちらっと見た Twitter の関連スレッド(本記事の内容とはあまり関係ない)
Goのforループが難しいことを表すコードです。https://t.co/1SGOedFxlb
— morikuni (@inukirom) November 22, 2018
- セレクタの仕様(本記事の内容とはあまり関係ない)