For-Range 構文の話
今回は小ネタでお送りします。
つか,以下の記事
の後半部分が何を問題にしているのか分からずしばらく悩んでしまった。 頭が悪くてゴメンペコン。
まず前提として Go 言語において「参照」のことは忘れよう。 見た目は参照ぽく振る舞う場合もあるしこのブログでも比喩表現として参照という言葉をよく使うが,言語仕様としては Go 言語に「参照」は存在しない。 Go 言語のインスタンスを表すものは「値」と「アドレッシング」であり,その「アドレッシング」の表現として(C 言語ではお馴染みの)ポインタを使うわけだ1。 したがって,コーディングを行う際はインスタンスの値とアドレッシングを常に頭に入れながら進めるとハマりにくい。 分からないならコードを書いてみればいいのだ。
では,最初に紹介した記事を参考に実際にコードを書いてみようか。
まず,以下のような型 A
を考える。
type A struct {
N string
}
func (a *A) GenFunc() func() {
return func() {
fmt.Printf("%s : %p\n", a.N, a)
}
}
A.GenFunc()
関数は型 A
の内容を標準出力に出力する関数を返す。
Method receiver が A
ではなく *A
とポインタ型になっているのがポイントである(洒落ちゃったてへぺろ)。
これを使って以下のコードを書いてみる。
package main
import "fmt"
type A struct {
N string
}
func (a *A) GenFunc() func() {
return func() {
fmt.Printf("%s : %p\n", a.N, a)
}
}
func main() {
as := []A{
{"foo"},
{"bar"},
}
for i := 0; i < len(as); i++ {
fmt.Printf("instance: %s : %p\n", as[i].N, &as[i])
as[i].GenFunc()()
}
}
このコードを実行すると
instance: foo : 0x40a0e0
foo : 0x40a0e0
instance: bar : 0x40a0e8
bar : 0x40a0e8
などと出力される。
GenFunc()
メソッドを呼び出す際に method receiver としての as[i]
の型が暗黙的に変換されていることに注意してほしい。
ここまでは OK かな。
では for-range 構文を使うとどうなるか。
試しに main
関数に for-range の制御ブロックを追加して比較してみよう。
func main() {
as := []A{
{"foo"},
{"bar"},
}
for i := 0; i < len(as); i++ {
fmt.Printf("instance: %s : %p\n", as[i].N, &as[i])
as[i].GenFunc()()
}
fmt.Println()
for _ , a := range as {
fmt.Printf("instance: %s : %p\n", a.N, &a)
a.GenFunc()()
}
}
これを実行するとこうなる。
instance: foo : 0x40a0e0
foo : 0x40a0e0
instance: bar : 0x40a0e8
bar : 0x40a0e8
instance: foo : 0x40c160
foo : 0x40c160
instance: bar : 0x40c160
bar : 0x40c160
ポインタ値からインスタンス a
はインスタンス as[i]
そのものではないことが分かる。
しかも a
は for-range ループの中で使い回されていていることも分かる。
つまり上の for-range 構文は
for i, a := 0, as[0]; i < len(as); i, a = i+1, as[i+1] {
fmt.Printf("instance: %s : %p\n", a.N, &a)
a.GenFunc()()
}
と実質的に同じなのだ2。
ここまで来れば
func main() {
as := []A{
{"foo"},
{"bar"},
}
fs := []func(){}
for _, a := range as {
fs = append(fs, a.GenFunc())
}
for _, f := range fs {
f()
}
}
の実行結果がどうなるか容易に想像がつくだろう。 以下の通りである。
bar : 0x40c128
bar : 0x40c128
つまり fs
に格納される関数は全て前半の for-range の中のインスタンス a
に帰属するメソッドであり,そのインスタンス a
の値は for-range ループの中で上書きされているのである。
じゃあどうすればいいかというと,一番簡単で場当たりな対処としては for-range ループの中でインスタンス a
のコピーを作ればよい。
func main() {
as := []A{
{"foo"},
{"bar"},
}
fs := []func(){}
for _ , a := range as {
aa := a //create copy instance
fs = append(fs, aa.GenFunc())
}
for _ , f := range fs {
f()
}
}
これで実行結果は
foo : 0x40c128
bar : 0x40c140
となる。 あるいは最初から素直に
func main() {
as := []A{
{"foo"},
{"bar"},
}
fs := []func(){}
for i := 0; i < len(as); i++ {
fs = append(fs, as[i].GenFunc())
}
for _ , f := range fs {
f()
}
}
とすれば要らんコピーも発生せず,実行結果も
foo : 0x40a0e0
bar : 0x40a0e8
となる。
最初に紹介した記事にも書かれている通り,インスタンスの値とポインタを混ぜるから危険なのであって混ぜなければ大丈夫(笑)
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。
- プログラミング言語C 第2版 ANSI規格準拠
- B.W. カーニハン (著), D.M. リッチー (著), 石田 晴久 (翻訳)
- 共立出版 1989-06-15
- 単行本
- 4320026926 (ASIN), 9784320026926 (EAN), 4320026926 (ISBN)
- 評価
通称 “K&R”。その筋の人々には「バイブル」と呼ばれる名著(当時は)。