確認に使ったテストコードは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を使うといいでしょう。