Ruby におけるオブジェクトの比較 (同一性, 同値性, 順序など)

プログラミング言語 Ruby

プログラミング言語 Ruby

最近 『プログラミング言語 Ruby』 を読んで Ruby の勉強をしてます。 せっかくなので気になったところなどをメモ程度に書き残しておこうかと思います。 この記事ではオブジェクトの比較に関して述べます。

はじめに : 同値性と同一性

オブジェクトを比較する際に、まず思いつく比較は 「等しいかどうか」 ということです。 しかし、一口に 「等しい」 と言っても、「値が等しい」 のか、「同一のものである」 のかといった違いがあります。

「値が等しいかどうか」 というのは同値性 (または等値性) といい、別々のオブジェクトであっても値が等しければ *1 それは同値であるといえます。 一方、「同一のものかどうか」 というのは同一性といい、同じオブジェクトかどうかが問題になります。 同一であれば同値である、というのは常に成り立ちますが、同値だからといって同一とは限りません。

以下のページが参考になるかと思います。

equal? メソッド

equal? メソッドは、Object クラスで定義されているインスタンスメソッドであり、引数を 1 つとります。 引数の参照先オブジェクトとレシーバが同一のオブジェクトかどうかをテストします。

str1 = str2 = String.new "aaa"
str3 = String.new "bbb"
# 同一のオブジェクトかどうか判断
str1.equal? str2 #=> true; str1 と str2 は同じオブジェクトを参照している
str1.equal? str3 #=> false; str1 と str3 は異なるオブジェクトを参照している
このメソッドをオーバーライドすることは可能ですが、あらゆるサブクラスでこのメソッドをオーバーライドしないという習慣になっています。

同一のオブジェクトかどうかの判断には、object_id__id__ が等しいかどうか確かめるという方法もあります。

str1.object_id == str2 #=> true
str1.object_id == str3 #=> false

== 演算子, != 演算子

== 演算子は、Object クラスにおいては equal? メソッドの別名として定義されています。 しかし、多くのクラスで == 演算子は再定義されており、多くの場合は同値性を判断するようになっています。 自分で新たにクラスを定義する際に、同値性の判定を行う必要があれば == 演算子をオーバーライドするようにしましょう。

String クラスでも == 演算子は同値性 (文字列の内容が同じかどうか *2 ) を判断するようにオーバーライドされています。

str1 = String.new "aaa"
str3 = String.new "aaa"
# 同値かどうかのテスト
str1 == str3 #=> true; str1 と str3 の参照先オブジェクトの値は同じ (両方とも "aaa" という文字列)
# 同値だが同一ではない
str1.equal? str3 #=> false; str1 と str3 は異なるオブジェクトを参照している

!= 演算子は、== の結果が偽 (false または nil) の場合は true を返し、== の結果が真のとき (偽でないとき) は false を返します。 ただし、Ruby 1.9 では != 演算子のオーバーライドが可能となった *3 ので、先祖クラスまたは自分自身が != 演算子をオーバーライドしている場合はこの限りではありません。

eql? メソッド

eql? メソッドは、Object クラスにおいては equal? メソッドの別名として定義されています。

このメソッドの重要な点は、Hash クラスにおいて 2 つのハッシュキーが等しいかどうかのテストに使用される というところです。 ハッシュキーとなる 2 つのオブジェクトの hash メソッドの戻り値が等しく、かつ eql? メソッドで比較した際に真となれば、2 つのハッシュキーは等しいとみなされます *4。 そのため、eql? メソッドを再定義する際は、一緒に hash メソッドを再定義する必要があります。 hash メソッドは、2 つのオブジェクトを eql? メソッドで比較して真になる場合に hash メソッドが同じ値を返すように定義しなければなりません。 eql? メソッドで比較して偽になる場合は hash メソッドが異なる値を返しても同じ値を返しても構いません。

== 演算子をオーバーライドする場合は、eql? メソッドを == 演算子の別名として定義することが多いです。 また、一部のクラスでは == 演算子よりも厳密な等値判定のために eql? メソッドを再定義しています。 例えば数値の場合、Fixnum と Float は数値的に同じであれば == 演算子で true になりますが、eql? メソッドは false を返します。

1 == 1.0 #=> true; 型が違っても数値的に同じであれば true
1.eql? 1.0 #=> false; 型が違うと false

=== 演算子

=== 演算子は case 等価演算子と呼ばれる演算子です。 case 文の中でターゲット値が when 節の値と等しいかどうかテストする際に使用されます *5。 明示的に === 演算子を使用することはあまりありません。

Object クラスでは === 演算子は == 演算子の別名として定義されています。 詳しくは下記記事が参考になります。

=~ 演算子, !~ 演算子

=~ 演算子Regexp と String (Ruby 1.9 では Symbol でも) でパターンマッチ (正規表現によるマッチング) を実行するように定義されています。 Object クラスでは常に false を返すように定義されており、基本的にはパターンマッチ以外には使用しません。 ユーザー定義クラスにおいてパターンマッチを実行したりおおよそ等しいという概念を必要とする場合には =~ 演算子をオーバーライドすれば良いでしょう。

!~ 演算子は =~ 演算子が偽 (nil または false) を返す場合は true を返し、それ以外の場合は false を返します。 ただし Ruby 1.9 では !~ 演算子をオーバーライドできるようになっており、オーバーライドした場合はこの限りではありません。

<=> 演算子, 大小関係

オブジェクトの大小関係をテストするための演算子は <=> 演算子です。 宇宙船の形に見えることから宇宙船演算子とも呼ばれます。 <=> 演算子は、普通は左辺の方が小さければ正の値 (普通は -1) を、両辺が等しければ 0 を、左辺の方が大きければ正の値 (普通は -1) を返すように定義します。 <=> 演算子を定義した上で Comparable モジュールをミックスインすると、自動的に以下の演算子が使用できるようになります。 <=> 演算子nil を返すときは、以下で定義された演算子は全て false を返します。

  • < 演算子 : 左辺が右辺より小さいかどうかをテスト
  • <= 演算子 : 左辺が右辺以下かどうかをテスト
  • <== 演算子 : 左辺と右辺が等しいかどうかテスト
  • >= 演算子 : 左辺が右辺以上かどうかテスト
  • > 演算子 : 左辺が右辺より大きいかどうかをテスト

<=> 演算子を定義し、Comparable モジュールをミックスインすると == 演算子が再定義されますが、それとは別に == 演算子を定義することも可能です。 すなわち、<=> 演算子が 0 を返すのに == 演算子が false になるようなオブジェクトや、<=> 演算子が 0 でない場合でも == 演算子が true になりえるオブジェクトを作成することが可能です。 しかし、基本的には <=> 演算子が 0 の場合にのみ == 演算子が true になるように定義しておくのが無難でしょう。

*1:「値が等しい」 というのをどう定義するか、という問題はとりあえず置いておきます。

*2:Ruby 1.9 の場合は、文字列のエンコーディングも同じかどうかチェックする

*3:Ruby 1.8 では不可能

*4:Ruby 1.9 で試したところ、同一のオブジェクトの場合は hash メソッドの戻り値が等しければ eql? メソッドが偽でも同じキーとして扱われました

*5:when 節の値がレシーバ、case 文のターゲット値が引数としてテストされます