RubyのModule#===について
==
とか===
とかequal
とか
==
での同値性判定は暗黙の型変換を行ってゆるーくふわっと判定するけど、===
は型変換を行わずに同値性判定を行う言語ってありますよね。
PHPとかJavaScriptとか。
一方で、==
はオブジェクトの同一性判定を行うため、同値性判定を行うにはa equals(b)
とか[a isEqualTo*:b]
とか書く言語もありますよね。
JavaとかObjective-Cとか。
Rubyのお話
Rubyの==
について
Rubyでは、==
はメソッドなので、同値性判定なのか同一性判定なのかもよくわからない可愛いやつです。
==
は同一性判定を行うように定義されていますが、同値性判定を行うように==
をオーバーライドすることを期待されています。*1
逆に、equal?
はオーバーライドしてはいけません。
Unlike ==, the equal? method should never be overridden by subclasses as it is used to determine object identity Class: BasicObject (Ruby 2.1.0)
例えばStringは==
で同値性を判定できます。
a = "hoge" b = "hoge" puts a.object_id #=> 70312919003820 puts b.object_id #=> 70312919003800 puts a.equal? b #=> false puts a == b #=> true
ただ、自前で実装したクラスに==
を定義していないと同値性判定ではなく同一性判定を行うので、ちゃんとオーバーライドしてないのに同値性判定を期待すると痛い目にあいます。
class Cat attr_reader :name, :age def initialize name, age @name = name @age = age end end mike = Cat.new("mike",5) kuro = Cat.new("kuro",3) puts mike != kuro #=> true puts [mike, kuro].include? Cat.new("mike",5) #=> false
Rubyの===
について
PHPやJavaScriptとは違い、Rubyの===
は「==
より厳密な同値性判定を行う」といった性質のものではありません。
===
はcase式のためにあるような演算子メソッドで、
case a when b then hogehoge() when c then fugafuga() end
case式の中でaとbを比較するために===
が使用されます。
ここで注意すべきなのは、a === b
ではなく、b === a
が呼ばれるということです。
これらは全く違います。
===
は単なる演算子ではなくメソッドなので、どちらがレシーバで、どちらが引数なのかはとても重要です。
メソッド呼び出しであることがわかりやすいように書き直すと、a.===(b)
ではなく、b.===(a)
が呼ばれます。
===
のオーバーライド時に一般に期待されるのは、「所属性」を返すことです。*2
b.===(a)
に求められるのは、「引数aは、bに所属しているか」を返すことです。
よくRubyのcase式の例で挙げられる、Range#===
は、Range#include?
、Range#member?
と全く同じです。*3
そのため、こんなクールなcase式を書けます。
case 1 when (1..3) then puts "Expected" else puts "Oops.." end #=> Expected
しかし、Rangeオブジェクトそのものをcase式で比較すると、意に反した動作をします。
(1..3) === (1..3)
、つまり(1..3).include? (1..3)
はfalse
だからです。
range = (1..3) case range when (1..3) then puts "Expected" else puts "Oops.." end #=> Oops..
Module#===について
たとえば、引数に名前か年齢を受け取るメソッドを考えてみます。 名前を渡されたら挨拶を、年齢を渡されたら年齢を表示するメソッドです。
def a_method name_or_age # 名前なら、"hello, name!"と出力 # 年齢なら、"You are age years old"と出力 end
そこで、引数の型チェックを行ってみようと思います。
ぱっと浮かぶのはこんなコードでしょうか。
def a_method name_or_age case name_or_age.class when String then puts "hello, #{name_or_age}" when Fixnum then puts "You are #{name_or_age} years old" else raise ArgumentError end end
name_or_age
のクラスによって分岐して、Stringなら挨拶を、Integerなら年齢を表示します。
これを実行してみます。
a_method "oropon" #=> in `a_method': ArgumentError (ArgumentError) a_method 27 #=> in `a_method': ArgumentError (ArgumentError)
動きません!StringでもFixnumでもないよ、と怒られてしまいます。
でも、==
で比較してみるとちゃんとtrue
が返ってきます。
puts "oropon".class == String #=> true puts 27.class == Fixnum #=> true
==
での比較はtrue
なのに、===
で比較するとfalse
なの?
==
は===
より厳格じゃないの??
YES!! 上でRangeの例を挙げたように、Rubyでは==
と===
は厳格さが違うのではなく、用途が違うんです!
先ほどのコードを書き直すと、こんな感じになります。
def a_method name_or_age case name_or_age when String then puts "hello, #{name_or_age}" when Fixnum then puts "You are #{name_or_age} years old" else raise ArgumentError end end a_method "oropon" #=> hello, oropon a_method 27 #=> You are 27 years old
caseの右にあるname_or_age.class
から、.class
が取れてname_or_age
だけになりました。
これだけで何故うまく動くのかの答えは、Module#===
の実装にあります。
StringやFixnumはClassクラスのインスタンスで、ClassクラスはModuleクラスのサブクラスです。Class#===
は、継承元のModule#===
が呼び出されます。
ドキュメントを見てみます。
self === obj -> bool
指定された
obj
が自身かそのサブクラスのインスタンスであるとき真を返します。 また、obj
が自身をインクルードしたクラスかそのサブクラスのイン スタンスである場合にも 真を返します。上記のいずれでもない場合にfalse
を返します。言い替えると
obj.kind_of?(self)
がtrue
の場合、true
を返します。このメソッドは主に case 文での比較に用いられます。 case ではクラス、モジュールの所属関係をチェックすることになります。
これで謎が解けました。
String === "oropon".class
は、"oropon".class.kind_of?(String)
ということで、これは「StringクラスはStringクラス*4のインスタンスですか?」と聞いているに等しいわけです。もちろん返り値はfalse
です。
ついでに、kind_of?
で比較するのでこう書き換えられます。
def a_method name_or_age case name_or_age when String then puts "hello, #{name_or_age}" when Integer then puts "You are #{name_or_age} years old" # Fixnum -> Integer else raise ArgumentError end end a_method "oropon" #=> hello, oropon a_method 27 #=> You are 27 years old
Fixnum
をInteger
に書き換えたことで、引数がFixnumでもBignumでも年齢として扱います。
以上、「case式は、==
が通っても===
は通らないことがあるから気をつけろよ!」「case式や===
の挙動は直感だけに頼らずちゃんと確認しろよ!」というお話でした。