The Go Blog
リフレクションの法則
はじめに
コンピューティングにおけるリフレクションとは、プログラムが自身の構造、特に型を通じて、自身の構造を調べることができる能力であり、メタプログラミングの一種です。また、それは混乱の大きな原因でもあります。
この記事では、Goでリフレクションがどのように機能するかを説明することで、物事を明確にしようと試みます。各言語のリフレクションモデルは異なります(そして多くの言語はリフレクションをまったくサポートしていません)が、この記事はGoに関するものなので、この記事の残りの部分では「リフレクション」という言葉は「Goにおけるリフレクション」を意味するものとします。
2022年1月追記:このブログ記事は2011年に書かれたもので、Goにおけるパラメトリックポリモーフィズム(別名ジェネリクス)の登場以前のものです。言語のその発展の結果として記事の重要な部分が間違っているということはありませんが、現代のGoに慣れている人を混乱させないように、いくつかの箇所で修正が加えられています。
型とインターフェース
リフレクションは型システムの上に構築されているため、Goの型に関する復習から始めましょう。
Goは静的型付け言語です。すべての変数には静的型があります。つまり、コンパイル時に既知で固定された正確に1つの型です: int、float32、*MyType、[]byteなどです。もし
type MyInt int
var i int
var j MyInt
と宣言すると、iはint型、jはMyInt型を持ちます。変数iとjは異なる静的型を持ち、同じ基底型を持っていても、変換なしで互いに代入することはできません。
重要な型のカテゴリの1つに、メソッドの固定されたセットを表すインターフェース型があります。(リフレクションを議論する際には、ポリモーフィックなコード内の制約としてのインターフェース定義の使用は無視できます。)インターフェース変数は、その値がインターフェースのメソッドを実装している限り、任意の具象(非インターフェース)値を格納できます。よく知られている例のペアは、io.Readerとio.Writer、ioパッケージのReaderとWriter型です
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
このシグネチャを持つRead(またはWrite)メソッドを実装する型は、io.Reader(またはio.Writer)を実装していると言われます。この議論の目的のために、それはio.Reader型の変数が、その型がReadメソッドを持つ任意の値を保持できることを意味します
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
rがどのような具象値を保持していても、rの型は常にio.Readerであることを明確にすることが重要です。Goは静的型付け言語であり、rの静的型はio.Readerです。
インターフェース型の極めて重要な例は、空のインターフェースです。
interface{}
またはその等価なエイリアス、
any
これは空のメソッドセットを表し、すべての値がゼロ個以上のメソッドを持つため、任意の値によって満たされます。
Goのインターフェースは動的型付けであると言う人もいますが、それは誤解を招きます。それらは静的型付けです。インターフェース型の変数は常に同じ静的型を持ち、実行時にインターフェース変数に格納されている値が型を変更しても、その値は常にインターフェースを満たします。
リフレクションとインターフェースは密接に関連しているため、これらすべてについて正確である必要があります。
インターフェースの表現
Russ Coxは、Goにおけるインターフェース値の表現について詳細なブログ記事を書いています。ここでその全貌を繰り返す必要はありませんが、簡略化された要約が必要です。
インターフェース型の変数はペアを格納します。変数に割り当てられた具象値と、その値の型記述子です。より正確には、値はインターフェースを実装する基になる具象データ項目であり、型はその項目の完全な型を記述します。たとえば、
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
の後、rは概念的に(tty, *os.File)という(値, 型)のペアを含んでいます。型*os.FileはRead以外のメソッドも実装していることに注意してください。インターフェース値はReadメソッドへのアクセスのみを提供しますが、内部の値はその値に関するすべての型情報を持っています。だからこそ、次のようなことができます
var w io.Writer
w = r.(io.Writer)
この代入における式は型アサーションです。それはrの中の項目がio.Writerも実装していることをアサートし、それによってwに代入できることを意味します。代入後、wはペア(tty, *os.File)を格納します。これはrが保持していたものと同じペアです。インターフェースの静的型は、インターフェース変数で呼び出すことができるメソッドを決定します。内部の具象値がより大きなメソッドセットを持っている場合でもです。
続けて、次のようにすることができます
var empty interface{}
empty = w
そして、私たちの空のインターフェース値emptyは、再び同じペア(tty, *os.File)を格納します。これは便利です。空のインターフェースはどんな値でも保持でき、その値について必要となるすべての情報を含んでいます。
(ここで型アサーションは必要ありません。なぜなら、wが空のインターフェースを満たしていることは静的に分かっているからです。ReaderからWriterに値を移動した例では、WriterのメソッドはReaderのサブセットではないため、明示的に型アサーションを使用する必要がありました。)
重要な詳細の1つは、インターフェース変数内のペアは常に(値, 具象型)の形式であり、(値, インターフェース型)の形式にはならないということです。インターフェースはインターフェース値を保持しません。
これでリフレクションする準備ができました。
リフレクションの第一法則
1. リフレクションはインターフェース値からリフレクションオブジェクトへ向かう。
基本的なレベルでは、リフレクションはインターフェース変数に格納された型と値のペアを検査するメカニズムに過ぎません。パッケージ reflectには、知っておくべき2つの型があります: TypeとValueです。これら2つの型はインターフェース変数の内容へのアクセスを提供し、reflect.TypeOfとreflect.ValueOfという2つの簡単な関数は、インターフェース値からreflect.Typeとreflect.Valueの部分を取得します。(また、reflect.Valueから対応するreflect.Typeを取得するのは簡単ですが、ここではValueとTypeの概念を分けておきましょう。)
TypeOfから始めましょう
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
このプログラムは次のように出力します
type: float64
プログラムがインターフェース値ではなく、float64変数xをreflect.TypeOfに渡しているように見えるため、ここでインターフェースがどこにあるのか疑問に思うかもしれません。しかし、それはそこにあります。godocが報告しているように、reflect.TypeOfのシグネチャには空のインターフェースが含まれています
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
reflect.TypeOf(x)を呼び出すと、xはまず空のインターフェースに格納され、それが引数として渡されます。reflect.TypeOfはその空のインターフェースをアンパックして型情報を回復します。
reflect.ValueOf関数は、もちろん、値を回復します(ここからは定型文を省略し、実行可能なコードのみに焦点を当てます)。
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
出力します
value: <float64 Value>
(Stringメソッドを明示的に呼び出しているのは、デフォルトではfmtパッケージがreflect.Valueの中を掘り下げて具象値を表示するからです。Stringメソッドはそうしません。)
reflect.Typeとreflect.Valueの両方には、それらを検査および操作するための多くのメソッドがあります。重要な例の1つは、ValueがTypeメソッドを持ち、reflect.ValueのTypeを返すことです。もう1つは、TypeとValueの両方がKindメソッドを持ち、格納されている項目の種類(Uint、Float64、Sliceなど)を示す定数を返すことです。また、ValueのIntやFloatのような名前のメソッドを使用すると、内部に格納されている値(int64やfloat64として)を取得できます
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
出力します
type: float64
kind is float64: true
value: 3.4
SetIntやSetFloatのようなメソッドもありますが、それらを使用するには、後述するリフレクションの第三法則のテーマである設定可能性を理解する必要があります。
リフレクションライブラリには、いくつか特筆すべき特性があります。第一に、APIをシンプルに保つため、Valueの「ゲッター」および「セッター」メソッドは、値を保持できる最大の型で動作します。たとえば、すべての符号付き整数に対してはint64です。つまり、ValueのIntメソッドはint64を返し、SetInt値はint64を受け取ります。関係する実際の型に変換する必要がある場合があります
var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.
第2の特性は、リフレクションオブジェクトのKindは静的型ではなく、基底の型を記述することです。ユーザー定義の整数型の値を含むリフレクションオブジェクトの場合、次のようになります。
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
xの静的型はintではなくMyIntですが、vのKindは依然としてreflect.Intです。言い換えれば、KindはintとMyIntを区別できませんが、Typeは区別できます。
リフレクションの第二法則
2. リフレクションはリフレクションオブジェクトからインターフェース値へ向かう。
物理的な反射と同様に、Goにおけるリフレクションもそれ自身の逆を生成します。
reflect.Valueが与えられれば、Interfaceメソッドを使ってインターフェース値を回復できます。実質的に、このメソッドは型と値の情報を再びインターフェース表現にパックし、結果を返します
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
結果として、次のように言えます
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
リフレクションオブジェクトvで表されるfloat64値を出力します。
しかし、もっと良いこともできます。fmt.Println、fmt.Printfなどの引数はすべて空のインターフェース値として渡され、以前の例で行ったように、fmtパッケージによって内部的にアンパックされます。したがって、reflect.Valueの内容を正しく出力するために必要なのは、Interfaceメソッドの結果を整形出力ルーチンに渡すことだけです。
fmt.Println(v.Interface())
(この記事が最初に書かれて以来、fmtパッケージに自動的にreflect.Valueをこのようにアンパックする変更が加えられたため、
fmt.Println(v)
同じ結果を得るために、ここでは.Interface()の呼び出しを保持します。)
私たちの値はfloat64なので、必要であれば浮動小数点形式も使用できます
fmt.Printf("value is %7.1e\n", v.Interface())
この場合、次が得られます
3.4e+00
ここでも、v.Interface()の結果をfloat64に型アサートする必要はありません。空のインターフェース値には具象値の型情報が内部に含まれており、Printfがそれを回復します。
要するに、InterfaceメソッドはValueOf関数の逆であり、その結果が常に静的型interface{}である点を除きます。
繰り返しになりますが、リフレクションはインターフェース値からリフレクションオブジェクトへ、そして再び戻る。
リフレクションの第三法則
3. リフレクションオブジェクトを変更するには、値が設定可能である必要がある。
第三法則は最も微妙で混乱しやすいですが、基本原則から始めれば理解するのは十分簡単です。
これは動作しないコードですが、研究する価値があります。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
このコードを実行すると、次のような不可解なメッセージでパニックになります。
panic: reflect.Value.SetFloat using unaddressable value
問題は、値7.1がアドレス可能でないことではなく、vが設定可能でないことです。設定可能性はリフレクションValueのプロパティであり、すべてのリフレクションValueがそれを持っているわけではありません。
ValueのCanSetメソッドは、Valueの設定可能性を報告します。私たちのケースでは、
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
出力します
settability of v: false
設定不可能なValueに対してSetメソッドを呼び出すとエラーになります。しかし、設定可能性とは何でしょうか?
設定可能性はアドレス可能性に少し似ていますが、より厳密です。これは、リフレクションオブジェクトが、リフレクションオブジェクトの作成に使用された実際のストレージを変更できるプロパティです。設定可能性は、リフレクションオブジェクトが元の項目を保持しているかどうかによって決まります。次のように言うと
var x float64 = 3.4
v := reflect.ValueOf(x)
xのコピーをreflect.ValueOfに渡します。したがって、reflect.ValueOfの引数として作成されたインターフェース値は、x自身ではなく、xのコピーです。したがって、
v.SetFloat(7.1)
というステートメントが成功した場合、vがxから作成されたように見えても、xは更新されません。代わりに、リフレクション値内に格納されたxのコピーが更新され、x自身は影響を受けません。これは混乱を招き、役に立たないため、不正であり、設定可能性はこの問題を回避するために使用されるプロパティです。
これが奇妙に思えるなら、それは違います。これは実は、珍しい装いをしたおなじみの状況です。xを関数に渡すことを考えてみてください
f(x)
xの値のコピーを渡したのであって、x自身を渡したわけではないため、fがxを変更できるとは期待しないでしょう。fがxを直接変更できるようにしたい場合は、関数の引数にxのアドレス(つまり、xへのポインタ)を渡す必要があります。
f(&x)
これは簡単で馴染み深いものであり、リフレクションも同様に機能します。リフレクションによってxを変更したい場合は、変更したい値へのポインタをリフレクションライブラリに与える必要があります。
それをやってみましょう。まず、通常通りxを初期化し、それへのポインタであるリフレクション値pを作成します。
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
これまでの出力は次の通りです。
type of p: *float64
settability of p: false
リフレクションオブジェクトpは設定可能ではありませんが、設定したいのはpではなく(実質的に)*pです。pが指すものにアクセスするには、ポインタを間接参照するValueのElemメソッドを呼び出し、結果をリフレクションValueであるvに保存します。
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
出力が示すように、vは設定可能なリフレクションオブジェクトになりました。
settability of v: true
そして、それがxを表すので、ついにv.SetFloatを使ってxの値を変更できます。
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
予想通り、出力は次の通りです。
7.1
7.1
リフレクションは理解するのが難しいかもしれませんが、Go言語がしていることを正確に実行しています。ただし、リフレクションのTypeとValueが何が起こっているかを隠す可能性があるだけです。リフレクションのValueは、それが表すものを変更するために、何かの「アドレス」が必要であることを覚えておいてください。
構造体
前の例では、vはポインタ自体ではなく、ポインタから派生したものでした。このような状況がよく発生するのは、リフレクションを使用して構造体のフィールドを変更する場合です。構造体のアドレスがあれば、そのフィールドを変更できます。
これは、構造体値tを分析する簡単な例です。後で変更したいため、構造体のアドレスを使用してリフレクションオブジェクトを作成します。次に、typeOfTをその型に設定し、簡単なメソッド呼び出しを使用してフィールドを反復処理します(詳細についてはパッケージ reflectを参照)。フィールド名は構造体型から抽出しますが、フィールド自体は通常のreflect.Valueオブジェクトであることに注意してください。
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
このプログラムの出力は次のとおりです。
0: A int = 23
1: B string = skidoo
ここで通り過ぎざまに導入された設定可能性に関するもう一点があります。Tのフィールド名は大文字(エクスポート済み)です。なぜなら、構造体のエクスポートされたフィールドだけが設定可能だからです。
sが設定可能なリフレクションオブジェクトを含んでいるため、構造体のフィールドを変更できます。
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
そして、結果は次のとおりです。
t is now {77 Sunset Strip}
もしプログラムを修正して、sが&tではなくtから作成された場合、SetIntとSetStringの呼び出しは失敗します。なぜなら、tのフィールドは設定可能ではないからです。
まとめ
リフレクションの法則をもう一度示します。
-
リフレクションはインターフェース値からリフレクションオブジェクトへ向かう。
-
リフレクションはリフレクションオブジェクトからインターフェース値へ向かう。
-
リフレクションオブジェクトを変更するには、値が設定可能である必要がある。
これらの法則を理解すれば、Goにおけるリフレクションははるかに使いやすくなりますが、依然として微妙な部分があります。これは強力なツールであり、注意して使用し、厳密に必要でない限り避けるべきです。
チャネルでの送受信、メモリの割り当て、スライスとマップの使用、メソッドと関数の呼び出しなど、リフレクションにはまだ多くのことがありますが、この記事は十分に長くなりました。これらのトピックのいくつかは、後の記事で取り上げます。