2017-09-08

RubyのModule#const_getについて調べてみた

Module#const_getはそのモジュール内に定義されている定数を取得することができるメソッドです。動的にインスタンスを生成したり、定数を参照するときに利用しますが、今回はModule#const_getのハマりポイントを紹介します。

まず結論

Module#const_getの問題点

以下のようなコードの場合、 Hoge::Fugaは定義されていないので uninitialized constant のエラーになってほしいのですが、トップレベルのFugaクラスを取得してしまいます。

#!/usr/bin/env ruby

module Hoge
end

class Fuga
end

puts Object.const_get 'Hoge::Fuga' 
# => Fuga
# Hoge::Fugaは定義されていないのでuninitialized constantのエラーになってほしい

これはModule#const_getのデフォルトの挙動が、以下のように継承ツリーやトップレベルの定義(Objectに属す)をたどって定数を探すようになっているためです。

  1. Hogeモジュール/クラスを探す => なければエラー
  2. Hogeモジュール/クラス内に定義されているFugaクラスを探す
  3. Fugaクラスがあれば、そのHoge::Fugaクラスを返す
  4. なければ、Hogeモジュールの継承ツリーをたどって、それぞれのスーパークラス・モジュール内でFuga定数を探す
  5. モジュールの場合、4で見つからなければトップレベルで定義されている定数を探す ※上記例だと、トップレベルにFugaが定義されているので、それを返している
  6. 2〜5を定数を探しきるまで繰り返す
ちなみにModule#const_getは継承ツリーをたどって何らかの定数が見つかった場合はModule#const_missingを発火させないようです↓ Railsのautoloadの仕組みはconst_missingを使っているので、例え正しいパス上に正しい名前空間のクラスが定義されていたとしても、トップレベルに同じ名前のクラスやモジュールが定義されていると、autoloadされずに継承ツリーをたどった結果の定数を返してしまいます。

解決方法

Module#const_getの第二引数のinheritは継承ツリーをたどるかどうか、モジュールの場合はさらにトップレベルの定数を探すかどうかを指定するパラメータとなっていて、デフォルトではtrueとなっています。これをfalseにすれば継承ツリーをたどらず、トップレベルの定数も探しに行かないようになります。

Object.const_get 'Hoge::Fuga', false # => Hoge::Fuga

ただ、この方法だと継承ツリーも見なくなるのでやや不便です。

ということで、継承ツリーの定数を参照しつつトップレベルの定数も探しに行かずに定数化するためのメソッドがActiveSupport::Inflector#constantizeになります。

'Hoge::Fuga'.constantize # => Hoge::Fuga

便利!

参考URL

このエントリーをはてなブックマークに追加