Rubyでリフレクションを用いてクラス名からインスタンスを生成
この間、C+Rubyで半分必須・半分遊びで作っているライブラリ(研究で必要な(?)実装を、趣味で使いたい技術を使って実装してる)の実装中、Rubyでクラス名を表す文字列からそのクラスのインスタンスを生成する必要が出てきたんだけど、ちょっとはまったのでそのメモ。
こんな感じのクラス群を定義したんだ。
class Base def initialize(name, data) ... end end class C1 < Base def initialize(name, data) super(name, data) ... end end class C2 < Base ...
initializeの引数のnameにはRubyのString型、dataには自分が定義したクラスのインスタンスが与えられると想定している。スーパークラスとしてBaseがあり、それを継承するサブクラスがいくつもある。そして、実行時に動的にある文字列を見て、その文字列が保持する名前のクラスのインスタンスを生成したかった。
で、こういう場合にはリフレクションを使うしかなく、Rubyでリフレクションならevalだろ、と思った僕は最初、次のように実装した。
obj = eval "#{cls_name}::new(#{name}, #{data})"
だけど、実行するとパーズエラーが起きた。それもそのはず、引数に与えているdataは自前定義のクラスで、しかもCで実装したRubyクラス(拡張ライブラリとして実装した)でto_sとか実装していなかったから。デフォルトの変な文字列に展開されて、それを引数に渡していたわけだから。きちんとは調べていないんだけど、dataのクラスにきちんとto_sメソッドなどを実装すれば問題なく実行できたかもしれないけど、Cで書いてあったので、そういうめんどくさい事はしたくなかった。
そこでクラスライブラリを調べたり、いろいろテストしてみたところ、次のことが分かった。
- クラス名を表す文字列をevalするとそのクラスのClassオブジェクトが取得できる
- Classオブジェクトには「new(args...)」と言うメソッドがあり、クラス定義でinitializeメソッドに設定した引数をこの関数の引数に与えて実行すると、そのクラスのインスタンスが生成される
この2つの情報を基に、次のように実装したところ、今度はちゃんとインスタンスが生成された。
begin cls = eval "#{cls_name}" cls.new(name, data) rescue クラスが存在しない場合の処理 end
例外処理しているのは与えられた名前に該当するクラスが存在しない場合に備えて。もちろん、evalは実行効率が悪いとのことなので、evalは最初に1度だけまとめて行い、出来上がったClassオブジェクトはHashだとか配列に入れて何度も再利用するべき。
この調査をしているとき、運悪くネットワークが使えなく、参考書も手元に無い状況にいたんだ。だからちょっと時間がかかってしまった。RubyのマニュアルはPCにダウンロード(htmlヘルプ&ri)してあったんだけど、それでも時間がかかったのは僕がマニュアルを読むのがへたくそだから?それともマニュアルが読みづらいから?正直どっちの形式のマニュアルも使いやすくはないんだよなぁ。