Goブログ
リフレクションの法則
はじめに
コンピューティングにおけるリフレクションとは、プログラムが自身の構造、特に型を通して調べる能力のことです。これはメタプログラミングの一形態でもあります。また、混乱の大きな原因でもあります。
この記事では、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>
(デフォルトではfmt
パッケージがreflect.Value
を掘り下げて内部の具体的な値を表示するため、String
メソッドを明示的に呼び出します。String
メソッドはそうではありません。)
reflect.Type
とreflect.Value
の両方には、それらを調べて操作するための多くのメソッドがあります。重要な例の1つは、Value
がreflect.Value
のType
を返すType
メソッドを持つことです。もう1つは、Type
とValue
の両方に、Uint
、Float64
、Slice
など、格納されているアイテムの種類を示す定数を返すKind
メソッドがあることです。また、Int
やFloat
のような名前のValue
のメソッドを使用すると、内部に格納された値(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
はType
が区別できる場合でも、int
をMyInt
から区別することはできません。
リフレクションの第二法則
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
の値のコピーを渡したので、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
メソッドを呼び出してポインタを間接参照し、その結果をv
というリフレクションのValue
に保存します。
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
リフレクションは理解するのが難しいかもしれませんが、言語が実行することとまったく同じことを行っています。ただし、何が起こっているのかを隠すことができるリフレクションのTypes
とValues
を介して行われます。リフレクションのValuesが、それらが表すものを変更するためには、何かのアドレスを必要とすることを覚えておいてください。
構造体
前の例では、v
はポインタ自体ではなく、ポインタから派生しただけでした。この状況が発生する一般的な方法は、構造体のフィールドを変更するためにリフレクションを使用する場合です。構造体のアドレスがあれば、そのフィールドを変更できます。
これは、構造体値t
を分析する簡単な例です。後で変更する必要があるため、構造体のアドレスを使用してリフレクションオブジェクトを作成します。次に、typeOfT
をその型に設定し、単純なメソッド呼び出しを使用してフィールドを反復処理します(詳細については、package 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
ここで簡単に導入された設定可能性について、もう1つポイントがあります。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
から作成されるようにプログラムを変更した場合、t
のフィールドは設定可能ではないため、SetInt
およびSetString
の呼び出しは失敗します。
結論
これがリフレクションの法則です。
-
リフレクションは、インターフェース値からリフレクションオブジェクトに進みます。
-
リフレクションは、リフレクションオブジェクトからインターフェース値に進みます。
-
リフレクションオブジェクトを変更するには、値が設定可能である必要があります。
これらの法則を理解すると、Goでのリフレクションの使用がはるかに簡単になりますが、依然として微妙なものです。これは強力なツールであり、慎重に使用し、厳密に必要な場合を除いて避ける必要があります。
チャネルでの送受信、メモリの割り当て、スライスとマップの使用、メソッドと関数の呼び出しなど、カバーしていないリフレクションについてはたくさんありますが、この記事は十分に長いです。これらのトピックのいくつかは、後の記事で取り上げます。