確認に使ったテストコードはgithubに置いてあります。
モジュール内でクラス拡張はできない
class String
def root_hello
"root hello"
end
end
module RootModule
class String
def root_module_hello
"root module hello"
end
end
end
モジュール内で"".root_module_hello
と使ってもNoMethodError
となります。これは""
を使うと、拡張されていないトップレベルのString
が生成されます。String.new.root_module_hello
とすばれ、拡張したメソッドを使うことができます。モジュール内でString
とクラス名を直接書いた場合は、モジュール内のString
が参照されます。
トップレベルのString
は文字列を生成するクラスですが、モジュール内で定義したString
はただの名前が同じの別クラスです。クラス拡張しているわけでもなく、この2つのString
は全く関係がありません。だから、モジュールの外から"".root_module_hello
は使えません。モジュールはクラス拡張を局所的にすることができます。
クラス拡張したならコンストラクタは正常だ
下記のコードは上は同じ名前のString
を定義しているだけです。initialize
を定義していないので、ArgumentError
が起きています。下は親クラスを指定して継承して、initialize
も受け継がれています。これは既存クラスを拡張しているわけではないので、トップレベルのString
とは別物です。
module Module1
class String; end
def self.create_instance
String.new "test" # ArgumentError
end
end
module Module2
class String < ::String
end
def self.create_instance
String.new "test"
end
def self.compare_string?
String.equal?(::String) # false
end
end
ダブロコロンを使った型判定
次はダブルコロンあり、なしでの型判定について見ていきます。
module NoExtend
def self.compare_string?
String === ::String # false
end
def self.compare_string2?
String === "" # true
end
def self.compare_string3?
::String === "" # true
end
end
module Extend
class String; end
def self.compare_string?
String === ::String # false
end
def self.compare_string2?
String === "" # false
end
def self.compare_string3?
::String === "" #true
end
end
NoExtend
もExtend
でも、String === ::String
はfalseを返します。 mod === obj
は、 is_a?
/kind_of?
と同じです。オブジェクトobj
がmod
クラスのインスタンスかを調べます。::String
はString
のインスタンスではなくクラスです。だからfalse
が返ります。モジュール内でクラス拡張した際は、型判定にはコロンをつけてトップレベルのクラスを使うようにした方がいいかもしれません。
2つのモジュールの違いは、String === ""
の返り値です。これは前述した内容と同じです。別のString
を定義したせいで、Extend
の中のString
はトップレベルのを参照しません。別のString
になっていることを次のコードで確認してみましょう。String.equal?(::String)
の部分です。
クラス拡張をすると、モジュールの名前空間に新しいクラスが登録される
module NoExtend
def self.equal_string?
String.equal?(::String) # true
end
def self.equal_string2?
String.equal?("") # false
end
def self.equal_string3?
::String.equal?("") # false
end
end
module Extend
class String; end
def self.equal_string?
String.equal?(::String) # false
end
def self.equal_string2?
String.equal?("") # false
end
def self.equal_string3?
::String.equal?("") # false
end
end
equal?
は==
の別名です。同じオブジェクトかを判断しています。NoExtend
の方ではtrue
のままです。クラス拡張を行わない場合は、String
のままでトップレベルのを参照できています。String.equal?("")
と::String.equal?("")
はクラスとインスタンスが同じかを比較しているので、クラス拡張や名前空間は関係なしにfalse
になります。
クラス拡張を局所的にするにはどうするか?
モジュール内で継承クラスを作るか、refine
とusing
を使います。
module NormalUsingModule
refine String do
def normal_using_hello
"normal using hello"
end
end
end
using NormalUsingModule
上記のusing
以降から"".normal_using_hello
が使えます。このファイルをrequire
した場合は、再度using
を記述しないといけません。using
のスコープはファイル内に留められています。
さて、どちらの方法を使えばいいのか?自作のライブラリでしか使わないなら直接クラス拡張を、クラス拡張自体を色々な場所で使いまわしたいならusing
を使うといいと思います。ただ、前者でも自作ライブラリの中で全体ではなく、さらに一部だけに影響を絞りたいならusing
を使うといいでしょう。