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

となる。

最初に紹介した記事にも書かれている通り,インスタンスの値とポインタを混ぜるから危険なのであって混ぜなければ大丈夫(笑)

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)

photo
プログラミング言語C 第2版 ANSI規格準拠
B.W. カーニハン (著), D.M. リッチー (著), 石田 晴久 (翻訳)
共立出版 1989-06-15
単行本
4320026926 (ASIN), 9784320026926 (EAN), 4320026926 (ISBN)
評価     

通称 “K&R”。その筋の人々には「バイブル」と呼ばれる名著(当時は)。

reviewed by Spiegel on 2018-12-07 (powered by PA-APIv5)


  1. 参照とポインタの決定的な違いは,参照は「機能」だがポインタは「値」である,という点であろう。値なのでポインタ自身をインスタンスとして表現し得るし「ポンタへのポインタ」みたいな記述もできる。この辺の感覚が掴めないと苦労するかもしれない。大昔の C 言語が全盛だった頃もポインタで躓く人は割といたからなぁ。 ↩︎

  2. 厳密には違う。つか,このコードを実際に書いて動かしてみれば分かるが “index out of range” の panic を吐いて落ちる。理由は各自で考えてみよう(笑) ↩︎