はじめに
コンピュータにおけるリフレクションとは、プログラムが自らの構造を調べる能力、特に型を通じてのものであり、メタプログラミングの一形態です。また、混乱の大きな原因でもあります。
この記事では、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
インターフェース型の非常に重要な例は空のインターフェースです:
``````bash
interface{}
`
またはその同等のエイリアス、
any
これは空のメソッドセットを表し、すべての値が満たすことができるため、すべての値はゼロ個以上のメソッドを持っています。
Goのインターフェースは動的型付けであると言う人もいますが、それは誤解を招くものです。インターフェース型の変数は常に同じ静的型を持ち、実行時にインターフェース変数に格納された値の型が変わることがあっても、その値は常にインターフェースを満たします。
リフレクションとインターフェースは密接に関連しているため、これらすべてについて正確である必要があります。
インターフェースの表現
ラッス・コックスはGoにおけるインターフェース値の表現についての詳細なブログ記事を書いています。ここで全ての話を繰り返す必要はありませんが、簡略化した要約が必要です。
インターフェース型の変数は、変数に割り当てられた具体的な値と、その値の型記述子のペアを格納します。より正確に言えば、その値はインターフェースを実装する基礎となる具体的なデータ項目であり、型はその項目の完全な型を記述します。たとえば、
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
``````bash
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. リフレクションはインターフェース値からリフレクションオブジェクトへ移行します。
基本的なレベルでは、リフレクションはインターフェース変数内に格納された型と値のペアを調べるためのメカニズムに過ぎません。始めるにあたり、reflectパッケージで知っておくべき2つの型があります:TypeとValueです。これら2つの型はインターフェース変数の内容にアクセスし、reflect.TypeOf
とreflect.ValueOf
という2つの簡単な関数がインターフェース値からreflect.Type
とreflect.Value
の部分を取得します。(また、reflect.Value
から対応するreflect.Type
に簡単にアクセスできますが、Value
とType
の概念は今のところ分けておきましょう。)
``````bash
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.ValueOf`````関数はもちろん、値を回復します(ここからはボイラープレートを省略し、実行可能なコードに焦点を当てます):
``````bash
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
`
は
value: <float64 Value>
と出力します。
(String
メソッドを明示的に呼び出すのは、デフォルトではfmt
パッケージがreflect.Value
を掘り下げて内部の具体的な値を表示するからです。String
メソッドはそうではありません。)
``````bash
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
と出力します。
リフレクションライブラリには、特筆すべきいくつかの特性があります。まず、APIをシンプルに保つために、`````Value`````の「ゲッター」と「セッター」メソッドは、値を保持できる最大の型で動作します。たとえば、すべての符号付き整数に対して`````int64`````です。つまり、`````Int`````メソッドは`````Value`````の`````int64`````を返し、`````SetInt`````値は`````int64`````を取ります。実際に関与する型に変換する必要がある場合があります:
``````bash
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.
`
リフレクションオブジェクトのKind
は、静的型ではなく基礎型を記述します。リフレクションオブジェクトがユーザー定義の整数型の値を含む場合、たとえば
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
<a name="the-second-law-of-reflection"></a>
## リフレクションの第二法則
<a name="2-reflection-goes-from-reflection-object-to-interface-value"></a>
## 2. リフレクションはリフレクションオブジェクトからインターフェース値へ移行します。
物理的なリフレクションと同様に、Goにおけるリフレクションはその逆を生成します。
`````reflect.Value`````を与えると、`````Interface`````メソッドを使用してインターフェース値を回復できます。実際、このメソッドは型と値の情報をインターフェース表現に再パックし、結果を返します:
``````bash
// 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
の特性であり、すべてのリフレクションValues
がそれを持っているわけではありません。
``````bash
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)
成功することが許可されていれば、x
を更新することはなく、v
はx
から作成されたように見えます。しかし、リフレクション値内に格納されたx
のコピーを更新することになります。x
自体は影響を受けません。それは混乱を招き、無意味であるため、違法であり、設定可能性はこの問題を回避するために使用される特性です。
これが奇妙に思える場合、それはそうではありません。実際、これは異常な衣装の中での馴染みのある状況です。x
を関数に渡すことを考えてみてください:
f(x)
``````bash
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
が指すものにアクセスするには、Elem
メソッドを呼び出し、ポインタを通じて間接的にアクセスし、Value
というリフレクションValue
に結果を保存します:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
今、v
は設定可能なリフレクションオブジェクトであり、出力が示すように、
settability of v: true
``````bash
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
`
出力は予想通り、
7.1
7.1
リフレクションは理解するのが難しいかもしれませんが、言語が行っていることを正確に行っており、リフレクションTypes
とValues
を通じて何が起こっているのかを隠すことがあります。ただし、リフレクション値は、表現するものを変更するために何かのアドレスが必要であることを忘れないでください。
構造体
前の例では、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
のフィールド名は大文字(エクスポートされた)であるという点がもう1つあります。構造体のエクスポートされたフィールドのみが設定可能です。
``````bash
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におけるリフレクションははるかに使いやすくなりますが、微妙さは残ります。これは強力なツールであり、注意して使用すべきであり、厳密に必要な場合を除いて避けるべきです。
リフレクションには、チャネルでの送受信、メモリの割り当て、スライスやマップの使用、メソッドや関数の呼び出しなど、まだ多くのことがありますが、この投稿は十分に長くなりました。これらのトピックのいくつかは、後の記事で取り上げます。