Module#const_get
はそのモジュール内に定義されている定数を取得することができるメソッドです。動的にインスタンスを生成したり、定数を参照するときに利用しますが、今回はModule#const_get
のハマりポイントを紹介します。
まず結論
- Railsを使っていれば
ActiveSupport::Inflector#constantize
を使うのが良い - そうでなければ
Module#const_get
をするときはeagerload、autoloadの設定や、継承ツリーやトップレベルで既に定義されているかどうかなどを考慮して使う。
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に属す)をたどって定数を探すようになっているためです。
- Hogeモジュール/クラスを探す => なければエラー
- Hogeモジュール/クラス内に定義されているFugaクラスを探す
- Fugaクラスがあれば、そのHoge::Fugaクラスを返す
- なければ、Hogeモジュールの継承ツリーをたどって、それぞれのスーパークラス・モジュール内でFuga定数を探す
- モジュールの場合、4で見つからなければトップレベルで定義されている定数を探す ※上記例だと、トップレベルにFugaが定義されているので、それを返している
- 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
- Ruby: const_defined? / const_get の罠 – CLARA ONLINE techblog
- How does Ruby’s Object#const_get actually work? - Stack Overflow
- instance method Module#const_get (Ruby 2.4.0)
- ruby/variable.c at 40a9922449d95dd62a5006fa269a5d143eb15c4d · ruby/ruby
- Rails の自動読み込みを支える技術
- Rails5: production環境でのAutoloadの廃止 - Qiita