Goブログ

リフレクションの法則

Rob Pike
2011年9月6日

はじめに

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

この記事では、Goでのリフレクションの仕組みを説明することで、物事を明確にしようと試みます。各言語のリフレクションモデルは異なり(多くの言語はまったくサポートしていません)、この記事はGoに関するものであるため、この記事の残りの部分では「リフレクション」という言葉は「Goでのリフレクション」を意味するものと解釈されるべきです。

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

型とインターフェース

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

Goは静的型付けです。すべての変数には静的型、つまりコンパイル時に知られ、固定されている正確に1つの型(intfloat32*MyType[]byteなど)があります。次のように宣言すると

type MyInt int

var i int
var j MyInt

iは型intを持ち、jは型MyIntを持ちます。変数ijは異なる静的型を持ち、基礎となる型は同じですが、変換なしに互いに代入することはできません。

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

// 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.FileRead以外のメソッドも実装することに注意してください。インターフェース値は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つの型、TypeValueがあります。これらの2つの型はインターフェース変数の内容へのアクセスを提供し、reflect.TypeOfreflect.ValueOfと呼ばれる2つの単純な関数は、インターフェース値からreflect.Typereflect.Valueの部分を取得します。(また、reflect.Valueから対応するreflect.Typeに簡単にアクセスできますが、今のところValueTypeの概念を分離しておきましょう。)

まずTypeOfから始めましょう

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

このプログラムは次のように出力します

type: float64

プログラムはインターフェース値ではなくfloat64変数xreflect.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.Typereflect.Valueの両方には、それらを調べて操作するための多くのメソッドがあります。重要な例の1つは、Valuereflect.ValueTypeを返すTypeメソッドを持つことです。もう1つは、TypeValueの両方に、UintFloat64Sliceなど、格納されているアイテムの種類を示す定数を返すKindメソッドがあることです。また、IntFloatのような名前のValueのメソッドを使用すると、内部に格納された値(int64float64として)を取得できます。

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

SetIntSetFloatのようなメソッドもありますが、それらを使用するには、以下で説明するリフレクションの第三法則の主題である設定可能性を理解する必要があります。

リフレクションライブラリには、特に注目に値するいくつかのプロパティがあります。まず、APIをシンプルに保つために、Valueの「ゲッター」および「セッター」メソッドは、値を保持できる最大の型で動作します。たとえば、すべての符号付き整数の場合はint64です。つまり、ValueIntメソッドは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であっても、vKindは依然としてreflect.Intです。言い換えれば、KindTypeが区別できる場合でも、intMyIntから区別することはできません。

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

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.Printlnfmt.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がそれを持っているわけではありません。

ValueCanSetメソッドは、Valueの設定可能性を報告します。この例では、

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

次のように出力します

settability of v: false

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

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

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

と書いた場合、xのコピーをreflect.ValueOfに渡すため、reflect.ValueOfへの引数として作成されるインターフェース値はx自体のコピーであり、xそのものではありません。したがって、もしステートメント

v.SetFloat(7.1)

が成功することが許されると、vxから作成されたように見えても、xは更新されません。代わりに、リフレクション値の中に格納されているxのコピーが更新され、x自体は影響を受けません。これは混乱を招き、役に立たないため、許可されておらず、この問題を回避するために使用されるのが設定可能性です。

これが奇妙に思えるかもしれませんが、そうではありません。実際には、珍しい装いをまとった見慣れた状況です。xを関数に渡すことを考えてみてください。

f(x)

xの値のコピーを渡したので、fxを変更できるとは期待しません。fxを直接変更させたい場合は、関数に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が指すものを取得するには、ValueElemメソッドを呼び出してポインタを間接参照し、その結果を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

リフレクションは理解するのが難しいかもしれませんが、言語が実行することとまったく同じことを行っています。ただし、何が起こっているのかを隠すことができるリフレクションのTypesValuesを介して行われます。リフレクションの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でのリフレクションの使用がはるかに簡単になりますが、依然として微妙なものです。これは強力なツールであり、慎重に使用し、厳密に必要な場合を除いて避ける必要があります。

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

次の記事: Go image package
前の記事: Two Go Talks: "Lexical Scanning in Go" and "Cuddle: an App Engine Demo"
ブログインデックス