Rubyのお勉強第4回、クラスの判別編
さて、前回Rubyの勉強記事を書いてからなななんと約2年程たつわけですが、そんなことはさておき久しぶりに勉強日記をつけてみます。
ある変数が何のクラスであるかを判別する処理を書くときにどう書けばいいのか迷ってます。
Perlで言うところの、
if ( ref $hoge eq 'Foo' ) { }
って処理のことです。
で、一応instance_of?ってのを発見。
# hoge変数がハッシュクラスであれば if hoge.instance_of?(Hash) end
これでバッチグーだと思ったのですが、二つの点で問題がありました。
まず、instance_of?に渡す引数は定義済みのクラスで無いといけない点。
# Fooクラスが定義されて無いので、 #「uninitialized constant Foo (NameError)」という例外発生 if hoge.instance_of?(Foo) end # Fooクラスが定義されてるので大丈夫 class Foo; end if hoge.instance_of?(Foo) end
上記の例のようにFooクラスが定義されてないと例外が発生するので、定義されてなくても判別するには、
if defined?(Foo) && hoge.instance_of?(Foo) end
といったようにdefined?を使って定義されているかどうかをチェックしなければならない。
ただし、defined?でのチェックだと今度は下記のような書き方をしたときに例外になる。
Foo = {} if defined?(Foo) && hoge.instance_of?(Foo) end
「`instance_of?': class or module required (TypeError)」という例外が発生。要はinstance_of?の引数にはclassかmoduleしか渡せないよってことだ。
PHPのclass_existsみたいにFooがクラスとして定義されてるかどうかをチェックする方法があればいいんだけど、ドキュメント見る限りではそういうのがなかったっぽい。うーん、見落としてるのか。
ただまぁ、上記のような書き方は一般的にしないのではないかと予測。
rubyの命名規約的にも変数名は全て小文字でやることになってると思うので先頭大文字で始まる変数を定義してる時点でアウトでしょう。
では、もうひとつ。文字列からクラス名を判断できない点。
どういう意味かというと、さっきの例外「`instance_of?': class or module required (TypeError)」とあるように、instance_of?には文字列を渡せないので、文字列からそのクラス名であるかどうかを判別できなかったのです。
# クラス名渡せゴルァを怒られる if hoge.instance_of?("Foo") end
文字列をクラス名と見なすようなことはできないんだろうか?調べてみたけどワカラズ。やはり見落としているかもしれない。
どうやらinstance_of?では不可能だと判断。別のやり方を模索してみた。
Object#classというのを発見。変数のクラス名を返すメソッドのようだ。これを使えば文字列からでも判別できる。やった!
if hoge.class.to_s == "Foo" end
という感じだ。しかも上記の場合、Fooが単なる文字列なのでFooというクラスが定義されてなくても例外にならない。
ということはinstance_of?よりも優秀ではないか。
・・・・と、思ったんだけど、ベンチをとってみてハッキリ。
require 'benchmark' class Foo; end class Hoge; end n = 100000 Benchmark.bm(16) do |x| x.report("string:") do n.times { Hoge.new.class.to_s == "Foo" } end x.report("def_instance_of:") do n.times { defined?(Foo) && Hoge.new.instance_of?(Foo) } end x.report("instance_of:") do n.times { Hoge.new.instance_of?(Foo) } end end __END__ user system total real string: 0.375000 0.000000 0.375000 ( 0.375000) def_instance_of: 0.312000 0.000000 0.312000 ( 0.313000) instance_of: 0.172000 0.000000 0.172000 ( 0.172000)
instance_of?の方が断然早い。
また、defined?でチェックしてからinstance_of?するのとclass.to_sでチェックするのは差ほど変わらない。
結論
上記のことから導き出される結論としては・・・
- instance_of?はクラスが定義されていることが保障されている時に使うと良い。
- class.to_sはクラスが定義されてない可能性がある場合に使うと良い。
- class.to_sは判定したいクラスが動的に変わる場合に使う良い。
ということになるのかなぁ。
instance_of?はつまり、ruby組み込みのクラスの判別するのに使うのが良さそう。HashとかArrayとかそーゆーの。
詳しい人突っ込みぷりーず。
やっぱ新しいことの勉強はたのしいね。
追記
コメントを頂いた
> rubyの命名規約的にも変数名は全て小文字でやることになってると思うので
定数は大文字からですね。
「class Foo」で宣言した「Foo」はClassクラスのインスタンスが入ってる定数、というわけです。
なるほど、これで合点行きました。リファレンスをよく見てみるとそのことが書いてますね・・・。
またクラス定義式はクラスオブジェクトの生成を行うと同時に、名前がクラス名である定数にクラスオブジェクトを代入する動作をします。クラス名を参照することは文法上は定数を参照していることになります。
class C end p C # => Chttp://www.ruby-lang.org/ja/man/html/_CAD1BFF4A4C8C4EABFF4.html#a.c4.ea.bf.f4
「class Foo」と宣言することで暗に定数FooにFooクラスオブジェクトが格納されているということですね。
> Fooがクラスとして定義されてるかどうかをチェックする方法
Foo.instance_of? Class でどうでしょうか。
上記の理由から単純にFooがClassであるかをinstance_of?使ってやればチェックできるってことになりますね。
> 文字列をクラス名と見なすようなことはできないんだろうか?
Module#const_getっていうのがありますです。
っていうわけで、
if hoge.instance_of? Module.const_get(’Foo’)
end
っていうこともできますね。
なるほどー、文字列から変換という意識しかなかったのでStringクラスを中心に探してました。
「class Foo」が定数ということを認識してればconst_getに辿りつくのは至極当然っすね。いやー、おもしろい。
ただしconst_getの仕様を見てみると存在しないクラス名の場合、例外になるようなので不確定な場合はやはりclass.to_sを使うのが無難のようです。
あと折角なので上記の方法でのベンチも追加してみました。
require 'benchmark' class Foo; end class Hoge; end n = 100000 Benchmark.bm(16) do |x| x.report("string:") do n.times { Hoge.new.class.to_s == "Foo" } end x.report("def_instance_of:") do n.times { defined?(Foo) && Hoge.new.instance_of?(Foo) } end x.report("instance_of:") do n.times { Hoge.new.instance_of?(Foo) } end x.report("const_get:") do n.times { Hoge.new.instance_of?( Module.const_get('Foo') ) } end x.report("def_const_get:") do n.times { Object.const_defined?('Foo') && Hoge.new.instance_of?( Module.const_get('Foo') ) } end end __END__ user system total real string: 0.360000 0.000000 0.360000 ( 0.360000) def_instance_of: 0.297000 0.000000 0.297000 ( 0.296000) instance_of: 0.156000 0.000000 0.156000 ( 0.157000) const_get: 0.266000 0.000000 0.266000 ( 0.265000) def_const_get: 0.375000 0.000000 0.375000 ( 0.375000)
というわけで、id:spiritlooseさんありがとうございました。