リフレクションの法則
The Laws of Reflection by Rob Pike
はじめに
コンピューティングにおけるリフレクション(reflection)とは、プログラムが自身の構造、特に型を通じて自らを検査する能力のことであり、メタプログラミングの一種です。そしてリフレクションは大きな混乱の種でもあります。
本稿ではGoにおいてリフレクションがどのように機能するかを説明することで、この混乱を解消することを試みます。言語ごとにリフレクションのモデルは異なり(そもそもサポートしていない言語も多くあります)、本稿はGoについて書かれたものなので、以降「リフレクション」という言葉は「Goにおけるリフレクション」を意味するものとします。
【2022年1月追記】本記事は2011年に書かれたもので、Goにパラメトリック多相(いわゆるジェネリクス)が導入される以前の内容です。この言語機能が加わったことで記事中の重要な内容が誤りになったわけではありませんが、現代のGoに慣れた読者を混乱させないよう、いくつかの箇所に手を加えています。
型とインターフェース
リフレクションは型システムの上に成り立っているので、まずGoにおける型についておさらいしておきましょう。
Goは静的型付け言語です。すべての変数は静的な型を持ちます。つまり、コンパイル時に判明していて、かつ固定された型がただ一つ存在するということです。int、float32、*MyType、[]byteなどがそれにあたります。次のように宣言したとすると、
type MyInt int
var i int
var j MyInt
i は int 型を、j は MyInt 型を持ちます。変数 i と j は、基底型(underlying type)が同じであっても異なる静的型を持っており、変換なしに互いへ代入することはできません。
型の重要なカテゴリの一つにインターフェース型があります。これは固定されたメソッドの集合を表します。(リフレクションについて議論する際には、多相的なコードにおける制約としてのインターフェース定義の使い方は無視して構いません。)インターフェース型の変数は、そのインターフェースのメソッドを実装している限り、任意の具象(非インターフェース)値を格納できます。よく知られた例としては、ioパッケージの Reader 型と Writer 型である io.Reader と io.Writer が挙げられます。
// Reader は基本となる Read メソッドをラップするインターフェースです。
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer は基本となる Write メソッドをラップするインターフェースです。
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)
// 以下同様に
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. リフレクションは、インターフェース値からリフレクションオブジェクトへと向かう
基本的なレベルでは、リフレクションとはインターフェース変数の中に格納された型と値のペアを検査するための機構に過ぎません。手始めに、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 を渡しているように見え、インターフェース値を渡しているようには見えないからです。しかし実際にはそこにインターフェースが存在しています。godocの説明にあるとおり、reflect.TypeOf のシグネチャには空のインターフェースが含まれています。
// TypeOf はinterface{}内の値のリフレクションTypeを返します。
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 はいずれも、それらを検査したり操作したりするための多数のメソッドを持っています。重要な例の一つは、Value が reflect.Value の Type を返す Type メソッドを持っていることです。もう一つは、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 のようなメソッドもありますが、これらを使うには、後述する第三の規則の主題である「セッタブル(settable)」について理解しておく必要があります。
リフレクションライブラリには、特筆すべき性質がいくつかあります。まず、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はuint64を返す
2つ目の性質は、リフレクションオブジェクトの Kind が表すのは静的型ではなく基底型(underlying type)だということです。リフレクションオブジェクトがユーザー定義の整数型の値を含んでいる場合、たとえば次のようなケースでは、
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
x の静的型が int ではなく MyInt であっても、v の Kind はやはり reflect.Int のままです。言い換えると、Type は int と MyInt を区別できますが、Kind はそれができません。
リフレクションの第二の規則
2. リフレクションは、リフレクションオブジェクトからインターフェース値へと向かう
物理的な反射(リフレクション)と同様に、Goにおけるリフレクションもまた自分自身の逆演算を生み出します。
reflect.Value が与えられたとき、Interface メソッドを使ってインターフェース値を復元できます。このメソッドは実質的に型と値の情報を再びインターフェース表現へと詰め直し、その結果を返します。
// Interface はvの値をinterface{}として返します。
func (v Value) Interface() interface{}
この結果として、次のように書けます。
y := v.Interface().(float64) // yは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) // エラー: パニックになります。
このコードを実行すると、次のような謎めいたメッセージとともにパニックになります。
panic: reflect.Value.SetFloat using unaddressable value
問題は値 7.1 がアドレス可能でないことではなく、v がセッタブルでないことです。セッタビリティ(settability)はリフレクションの 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 メソッドを呼ぶのはエラーです。では、セッタビリティとは何でしょうか。
セッタビリティはアドレス可能性(addressability)に少し似ていますが、より厳格な性質です。これは、リフレクションオブジェクトがそのリフレクションオブジェクトを生成する際に使われた実際のストレージを変更できるという性質です。セッタビリティは、リフレクションオブジェクトが元の項目そのものを保持しているかどうかによって決まります。次のように書いたとき、
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)
f が x を変更できるとは期待しないはずです。なぜなら、渡したのは x の値のコピーであって、x そのものではないからです。もし f に x を直接変更してほしければ、関数に x のアドレス(つまり x へのポインタ)を渡さなければなりません。
f(&x)
これは単純でおなじみのことであり、リフレクションも同じように動作します。もしリフレクションを使って x を変更したいなら、変更したい値へのポインタをリフレクションライブラリに渡す必要があります。
実際にやってみましょう。まずいつものように x を初期化し、次にそれを指すリフレクション値を作ります。これを p と呼びます。
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注: 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
そして v は x を表しているので、ようやく v.SetFloat を使って x の値を変更できます。
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
予想どおり、出力は次のようになります。
7.1
7.1
リフレクションは理解しづらいことがありますが、実際にはGoの言語そのものが行っていることと全く同じことをしているだけです。ただし、リフレクションの Type や 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 から作られるようにしていたら、t のフィールドがセッタブルでなくなるため、SetInt と SetString の呼び出しは失敗していたでしょう。
まとめ
ここでもう一度、リフレクションの規則を挙げておきます。
- リフレクションは、インターフェース値からリフレクションオブジェクトへと向かう。
- リフレクションは、リフレクションオブジェクトからインターフェース値へと向かう。
- リフレクションオブジェクトを変更するには、その値がセッタブルでなければならない。
これらの規則を理解すれば、Goにおけるリフレクションはずっと使いやすくなります。ただし、それでも依然として微妙な部分は残ります。リフレクションは強力な道具であり、慎重に使うべきものであって、どうしても必要な場合を除いて避けるべきです。
リフレクションにはまだまだ紹介しきれていないことがたくさんあります。チャネルでの送受信、メモリの割り当て、スライスやマップの利用、メソッドや関数の呼び出しなどです。しかし、この記事はもう十分に長くなりました。これらのトピックのいくつかについては、また別の記事で取り上げる予定です。
By Rob Pike