はじめに

コンピュータにおけるリフレクションとは、プログラムが自らの構造を調べる能力、特に型を通じてのものであり、メタプログラミングの一形態です。また、混乱の大きな原因でもあります。

この記事では、Goにおけるリフレクションの動作を説明することで、物事を明確にしようと試みます。各言語のリフレクションモデルは異なり(多くの言語は全くサポートしていません)、この記事はGoに関するものですので、この記事の残りの部分では「リフレクション」という言葉は「Goにおけるリフレクション」を意味するものとしてください。

2022年1月追加の注記:このブログ記事は2011年に書かれ、Goにおけるパラメトリックポリモーフィズム(いわゆるジェネリクス)よりも前のものです。その言語の発展によって、記事の重要な部分が不正確になったわけではありませんが、現代のGoに精通している人を混乱させないように、いくつかの箇所が調整されています。

型とインターフェース

リフレクションは型システムに基づいているため、Goにおける型についての復習から始めましょう。

Goは静的型付けです。すべての変数には静的型があり、コンパイル時に知られ、固定された正確に1つの型があります:intfloat32*MyType[]byteなどです。もし私たちが宣言した場合、

  1. type MyInt int
  2. var i int
  3. var j MyInt

その場合、iの型はintであり、jの型はMyIntです。変数ijは異なる静的型を持ち、同じ基礎型を持っていても、変換なしに互いに代入することはできません。

型の重要なカテゴリの1つはインターフェース型であり、固定されたメソッドのセットを表します。(リフレクションについて議論する際には、ポリモーフィックコード内の制約としてのインターフェース定義の使用を無視できます。)インターフェース変数は、その値がインターフェースのメソッドを実装している限り、任意の具体的(非インターフェース)値を格納できます。よく知られた例のペアは、io.Readerio.Writerであり、ioパッケージの型ReaderWriterです:

  1. // Reader is the interface that wraps the basic Read method.
  2. type Reader interface {
  3. Read(p []byte) (n int, err error)
  4. }
  5. // Writer is the interface that wraps the basic Write method.
  6. type Writer interface {
  7. Write(p []byte) (n int, err error)
  8. }

この署名を持つRead(またはWrite)メソッドを実装する任意の型は、io.Reader(またはio.Writer)を実装すると言われます。この議論の目的上、io.Reader型の変数は、Readメソッドを持つ任意の値を保持できることを意味します:

  1. var r io.Reader
  2. r = os.Stdin
  3. r = bufio.NewReader(r)
  4. r = new(bytes.Buffer)
  5. // and so on
  1. インターフェース型の非常に重要な例は空のインターフェースです:
  2. ``````bash
  3. interface{}
  4. `

またはその同等のエイリアス、

  1. any

これは空のメソッドセットを表し、すべての値が満たすことができるため、すべての値はゼロ個以上のメソッドを持っています。

Goのインターフェースは動的型付けであると言う人もいますが、それは誤解を招くものです。インターフェース型の変数は常に同じ静的型を持ち、実行時にインターフェース変数に格納された値の型が変わることがあっても、その値は常にインターフェースを満たします。

リフレクションとインターフェースは密接に関連しているため、これらすべてについて正確である必要があります。

インターフェースの表現

ラッス・コックスはGoにおけるインターフェース値の表現についての詳細なブログ記事を書いています。ここで全ての話を繰り返す必要はありませんが、簡略化した要約が必要です。

インターフェース型の変数は、変数に割り当てられた具体的な値と、その値の型記述子のペアを格納します。より正確に言えば、その値はインターフェースを実装する基礎となる具体的なデータ項目であり、型はその項目の完全な型を記述します。たとえば、

  1. var r io.Reader
  2. tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
  3. if err != nil {
  4. return nil, err
  5. }
  6. r = tty
  1. ``````bash
  2. var w io.Writer
  3. w = r.(io.Writer)
  4. `

この代入の式は型アサーションです。r内の項目がio.Writerを実装していることを主張しており、したがってwに代入できます。代入後、wはペア(tty*os.File)を含むことになります。これはrに保持されていたのと同じペアです。インターフェースの静的型は、インターフェース変数で呼び出すことができるメソッドを決定しますが、内部の具体的な値はより大きなメソッドセットを持っている場合があります。

続けて、次のことができます:

  1. var empty interface{}
  2. empty = w

そして、私たちの空のインターフェース値emptyは再びその同じペア(tty*os.File)を含むことになります。これは便利です:空のインターフェースは任意の値を保持でき、その値に関するすべての情報を含んでいます。

(ここでは型アサーションは必要ありません。wが空のインターフェースを満たすことが静的に知られているからです。ReaderからWriterに値を移動した例では、WriterのメソッドがReaderのサブセットではないため、明示的に型アサーションを使用する必要がありました。)

重要な詳細は、インターフェース変数内のペアは常に(値、具体的な型)の形式を持ち、(値、インターフェース型)の形式を持つことはできないということです。インターフェースはインターフェース値を保持しません。

さて、リフレクションの準備が整いました。

リフレクションの第一法則

1. リフレクションはインターフェース値からリフレクションオブジェクトへ移行します。

基本的なレベルでは、リフレクションはインターフェース変数内に格納された型と値のペアを調べるためのメカニズムに過ぎません。始めるにあたり、reflectパッケージで知っておくべき2つの型があります:TypeValueです。これら2つの型はインターフェース変数の内容にアクセスし、reflect.TypeOfreflect.ValueOfという2つの簡単な関数がインターフェース値からreflect.Typereflect.Valueの部分を取得します。(また、reflect.Valueから対応するreflect.Typeに簡単にアクセスできますが、ValueTypeの概念は今のところ分けておきましょう。)

  1. ``````bash
  2. package main
  3. import (
  4. "fmt"
  5. "reflect"
  6. )
  7. func main() {
  8. var x float64 = 3.4
  9. fmt.Println("type:", reflect.TypeOf(x))
  10. }
  11. `

このプログラムは

  1. type: float64

と出力します。

インターフェースがどこにあるのか不思議に思うかもしれません。プログラムはfloat64変数xreflect.TypeOfに渡しているように見えますが、インターフェース値ではありません。しかし、そこにあります。godocが報告するようにreflect.TypeOfのシグネチャには空のインターフェースが含まれています:

  1. // TypeOf returns the reflection Type of the value in the interface{}.
  2. func TypeOf(i interface{}) Type
  1. `````reflect.ValueOf`````関数はもちろん、値を回復します(ここからはボイラープレートを省略し、実行可能なコードに焦点を当てます):
  2. ``````bash
  3. var x float64 = 3.4
  4. fmt.Println("value:", reflect.ValueOf(x).String())
  5. `

  1. value: <float64 Value>

と出力します。

Stringメソッドを明示的に呼び出すのは、デフォルトではfmtパッケージがreflect.Valueを掘り下げて内部の具体的な値を表示するからです。Stringメソッドはそうではありません。)

  1. ``````bash
  2. var x float64 = 3.4
  3. v := reflect.ValueOf(x)
  4. fmt.Println("type:", v.Type())
  5. fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
  6. fmt.Println("value:", v.Float())
  7. `

  1. type: float64
  2. kind is float64: true
  3. value: 3.4

と出力します。

  1. リフレクションライブラリには、特筆すべきいくつかの特性があります。まず、APIをシンプルに保つために、`````Value`````の「ゲッター」と「セッター」メソッドは、値を保持できる最大の型で動作します。たとえば、すべての符号付き整数に対して`````int64`````です。つまり、`````Int`````メソッドは`````Value``````````int64`````を返し、`````SetInt`````値は`````int64`````を取ります。実際に関与する型に変換する必要がある場合があります:
  2. ``````bash
  3. var x uint8 = 'x'
  4. v := reflect.ValueOf(x)
  5. fmt.Println("type:", v.Type()) // uint8.
  6. fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
  7. x = uint8(v.Uint()) // v.Uint returns a uint64.
  8. `

リフレクションオブジェクトのKindは、静的型ではなく基礎型を記述します。リフレクションオブジェクトがユーザー定義の整数型の値を含む場合、たとえば

  1. type MyInt int
  2. var x MyInt = 7
  3. v := reflect.ValueOf(x)
  1. <a name="the-second-law-of-reflection"></a>
  2. ## リフレクションの第二法則
  3. <a name="2-reflection-goes-from-reflection-object-to-interface-value"></a>
  4. ## 2. リフレクションはリフレクションオブジェクトからインターフェース値へ移行します。
  5. 物理的なリフレクションと同様に、Goにおけるリフレクションはその逆を生成します。
  6. `````reflect.Value`````を与えると、`````Interface`````メソッドを使用してインターフェース値を回復できます。実際、このメソッドは型と値の情報をインターフェース表現に再パックし、結果を返します:
  7. ``````bash
  8. // Interface returns v's value as an interface{}.
  9. func (v Value) Interface() interface{}
  10. `

その結果、

  1. y := v.Interface().(float64) // y will have type float64.
  2. fmt.Println(y)

と出力して、リフレクションオブジェクトvによって表されるfloat64値を印刷できます。

しかし、さらに良いことができます。fmt.Printlnfmt.Printfなどへの引数はすべて空のインターフェース値として渡され、fmtパッケージによって内部的に展開されます。したがって、reflect.Valueの内容を正しく印刷するには、Interfaceメソッドの結果をフォーマットされた印刷ルーチンに渡すだけで済みます:

  1. fmt.Println(v.Interface())

(この記事が最初に書かれたとき、fmtパッケージに変更が加えられ、reflect.Valueのように自動的に展開されるようになったため、同じ結果を得るために

  1. fmt.Println(v)

と言うことができましたが、明確さのために.Interface()呼び出しをここに保持します。)

私たちの値がfloat64であるため、必要に応じて浮動小数点形式を使用することもできます:

  1. fmt.Printf("value is %7.1e\n", v.Interface())

この場合、得られるのは

  1. 3.4e+00

です。

再度、v.Interface()の結果をfloat64に型アサートする必要はありません。空のインターフェース値には具体的な値の型情報が含まれており、Printfがそれを回復します。

要するに、InterfaceメソッドはValueOf関数の逆であり、その結果は常に静的型interface{}です。

繰り返しますが、リフレクションはインターフェース値からリフレクションオブジェクトへ、そしてその逆へ移行します。

リフレクションの第三法則

3. リフレクションオブジェクトを変更するには、値が設定可能でなければなりません。

第三法則は最も微妙で混乱を招くものですが、基本原則から始めれば理解は容易です。

ここに動作しないコードがありますが、研究する価値があります。

  1. var x float64 = 3.4
  2. v := reflect.ValueOf(x)
  3. v.SetFloat(7.1) // Error: will panic.

このコードを実行すると、暗号的なメッセージでパニックになります

  1. panic: reflect.Value.SetFloat using unaddressable value

問題は、値7.1がアドレス可能でないことではなく、vが設定可能でないことです。設定可能性はリフレクションValueの特性であり、すべてのリフレクションValuesがそれを持っているわけではありません。

  1. ``````bash
  2. var x float64 = 3.4
  3. v := reflect.ValueOf(x)
  4. fmt.Println("settability of v:", v.CanSet())
  5. `

  1. settability of v: false

と出力します。

設定可能でないValueSetメソッドを呼び出すことはエラーです。しかし、設定可能性とは何でしょうか?

設定可能性はアドレス可能性に似ていますが、より厳格です。リフレクションオブジェクトがリフレクションオブジェクトを作成するために使用された実際のストレージを変更できる特性です。設定可能性は、リフレクションオブジェクトが元の項目を保持しているかどうかによって決まります。私たちが

  1. var x float64 = 3.4
  2. v := reflect.ValueOf(x)

と言うと、xのコピーをreflect.ValueOfに渡すので、reflect.ValueOfへの引数として作成されたインターフェース値はxのコピーであり、xそのものではありません。したがって、次の文が

  1. v.SetFloat(7.1)

成功することが許可されていれば、xを更新することはなく、vxから作成されたように見えます。しかし、リフレクション値内に格納されたxのコピーを更新することになります。x自体は影響を受けません。それは混乱を招き、無意味であるため、違法であり、設定可能性はこの問題を回避するために使用される特性です。

これが奇妙に思える場合、それはそうではありません。実際、これは異常な衣装の中での馴染みのある状況です。xを関数に渡すことを考えてみてください:

  1. f(x)
  1. ``````bash
  2. f(&x)
  3. `

これは簡単で馴染みのあることであり、リフレクションも同じように機能します。リフレクションを通じてxを変更したい場合、リフレクションライブラリに変更したい値のポインタを渡さなければなりません。

それを行いましょう。まず、xを通常通り初期化し、その後、pと呼ばれるそれを指すリフレクション値を作成します。

  1. var x float64 = 3.4
  2. p := reflect.ValueOf(&x) // Note: take the address of x.
  3. fmt.Println("type of p:", p.Type())
  4. fmt.Println("settability of p:", p.CanSet())

これまでの出力は

  1. type of p: *float64
  2. settability of p: false

リフレクションオブジェクトpは設定可能ではありませんが、設定したいのはpではなく、実質的には*pです。pが指すものにアクセスするには、Elemメソッドを呼び出し、ポインタを通じて間接的にアクセスし、ValueというリフレクションValueに結果を保存します:

  1. v := p.Elem()
  2. fmt.Println("settability of v:", v.CanSet())

今、vは設定可能なリフレクションオブジェクトであり、出力が示すように、

  1. settability of v: true
  1. ``````bash
  2. v.SetFloat(7.1)
  3. fmt.Println(v.Interface())
  4. fmt.Println(x)
  5. `

出力は予想通り、

  1. 7.1
  2. 7.1

リフレクションは理解するのが難しいかもしれませんが、言語が行っていることを正確に行っており、リフレクションTypesValuesを通じて何が起こっているのかを隠すことがあります。ただし、リフレクション値は、表現するものを変更するために何かのアドレスが必要であることを忘れないでください。

構造体

前の例では、vはポインタそのものではなく、単にポインタから派生したものでした。この状況が発生する一般的な方法は、構造体のフィールドを変更するためにリフレクションを使用することです。構造体のアドレスがあれば、そのフィールドを変更できます。

ここに構造体値tを分析する簡単な例があります。構造体のアドレスでリフレクションオブジェクトを作成します。後でそれを変更したいからです。その後、typeOfTをその型に設定し、簡単なメソッド呼び出しを使用してフィールドを反復処理します(詳細はreflectパッケージを参照してください)。フィールドの名前は構造体型から抽出しますが、フィールド自体は通常のreflect.Valueオブジェクトです。

  1. type T struct {
  2. A int
  3. B string
  4. }
  5. t := T{23, "skidoo"}
  6. s := reflect.ValueOf(&t).Elem()
  7. typeOfT := s.Type()
  8. for i := 0; i < s.NumField(); i++ {
  9. f := s.Field(i)
  10. fmt.Printf("%d: %s %s = %v\n", i,
  11. typeOfT.Field(i).Name, f.Type(), f.Interface())
  12. }

このプログラムの出力は

  1. 0: A int = 23
  2. 1: B string = skidoo

です。

ここで、Tのフィールド名は大文字(エクスポートされた)であるという点がもう1つあります。構造体のエクスポートされたフィールドのみが設定可能です。

  1. ``````bash
  2. s.Field(0).SetInt(77)
  3. s.Field(1).SetString("Sunset Strip")
  4. fmt.Println("t is now", t)
  5. `

そして、結果は次のとおりです:

  1. t is now {77 Sunset Strip}

もしstから作成され、&tからではないようにプログラムを修正した場合、SetIntSetStringへの呼び出しは失敗します。tのフィールドは設定可能ではないからです。

結論

ここにリフレクションの法則を再掲します:

  • リフレクションはインターフェース値からリフレクションオブジェクトへ移行します。
  • リフレクションはリフレクションオブジェクトからインターフェース値へ移行します。
  • リフレクションオブジェクトを変更するには、値が設定可能でなければなりません。

これらの法則を理解すると、Goにおけるリフレクションははるかに使いやすくなりますが、微妙さは残ります。これは強力なツールであり、注意して使用すべきであり、厳密に必要な場合を除いて避けるべきです。

リフレクションには、チャネルでの送受信、メモリの割り当て、スライスやマップの使用、メソッドや関数の呼び出しなど、まだ多くのことがありますが、この投稿は十分に長くなりました。これらのトピックのいくつかは、後の記事で取り上げます。