webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

Rubyをもう一歩進んで勉強する12章(4つの等値)

変数同士が等しいかどうかRubyで調べる手段をご存知でしょうか。

数値同士、文字列同士なら言わずと知れた==を使えばいいですね。ほかにもRubyにはeql?メソッドなどが存在します。

ですが、すべて同じ結果を返すわけではありません。何をもって等しいとするかの定義が少しずつ異なります。

"str" == "str" #=> true

"str".equal?("str") #=> false

そこで今回は4つの「等しい」について書いていきます。

equal?メソッド

equal?メソッドは比較する二つが全く同じオブジェクトであることをチェックします。

全く同じとは、参照しているオブジェクトが同じobject_idを持つかどうか、すなわち、ポインタが同じメモリ上のアドレスを指しているかどうかです。

スポンサーリンク

a = "str"
b = a
c = a.dup
a.equal?(b) #=> true
a.equal?(c) #=> false

ここでb = aはbがaと同じオブジェクトを参照するというのに対し、c = a.dupは中身がaと同じで全く新しいオブジェクトを作っています。

したがって、bはaと全く同じになりますが、cは同じ値なだけで別のオブジェクトになり、equal?メソッドはfalseを返します。

equal?メソッドは既存のクラスで使われているため、オーバーライドして異なる実装を与えるのはよくありません。

両者が等しいかどうかは次の==演算子を使います。

==演算子

プログラミングではお馴染みですが、数値オブジェクトが同じ値かどうか、文字列オブジェクトでは同じ文字列かどうかを比較します。

1 == 1.0 #=> true
"str" == "str" #=> true

これらはFixnumクラスやStringクラスで独自の実装を与えられていますが、デフォルトの実装、すなわち独自で定義したクラスのようにObjectクラスを継承しているクラスではequal?メソッドと同じ実装になっています。

なので、

class User
  attr_accessor :name, :age
  def initialize(name, age=0)
    @name = name
    @age = age
  end
end

a = User.new("user1")
b = User.new("user1")

a == b #=> false

この例ではUserクラスの==演算子がデフォルトの実装のままのため、内部でequal?メソッドが使われています。

したがって、別のインスタンスを指すa,bは==演算子ではfalseになってしまいます。

もちろんこれはあまりうれしくないので、==を使いたいときはオーバーライドして何をもって等しいかを与えてあげる必要があります。

なので、

class User
  attr_accessor :name, :age
  def initialize(name, age=0)
    @name = name
    @age = age
  end

  def ==(v)
    v.respond_to?(:name) && name == v.name
  end
end

a = User.new("user1")
b = User.new("user1")

a == b #=> true

Userクラスに名前が同じときは等しいとみなすという実装をしました。

respond_to?メソッドは引数の名前のメソッドが呼び出せるかどうかをチェックしています。これはvがnameメソッドを持たないクラスのインスタンスが来た時用です。受け付ける範囲は狭くなりますが、v.class == "User"とかでも大丈夫です。

あるいは独自の例外を作っておいて、v.respond_to?(:name)がfalseの時には例外を発生させるのもいいですね。

eql?メソッド

次はeql?メソッドについてです。

eql?メソッドはHashクラスがキーとして使われるオブジェクトが等しいかどうかをチェックするときに使います。

Rubyではハッシュのキーに文字列やシンボル意外に任意のオブジェクトを使うことができるので、例えば上記のUserクラスのインスタンスa,bを使って、

hash = {a => 1}
hash[b] = 2

とすることもできます。

では、このhashは結果どうなるでしょうか?実は次のようになります。

{#<User:0x000000033484a8 @name="user1", @age=0>=>1, #<User:0x000000033deb10 @name="user1", @age=0>=>2}

eql?メソッドではデフォルトでequal?メソッドと同じ比較を行います。なので、

c = a
hash[c] = 3

とすると、cはaと同じオブジェクトなのでハッシュの一つ目の値が3に更新されます。

{#<User:0x000000033484a8 @name="user1", @age=0>=>3, #<User:0x000000033deb10 @name="user1", @age=0>=>2}

ただキーとして使うにはequal?メソッドは若干厳しすぎるので独自の実装を与える必要があります。

それでは、独自実装し、ハッシュのキーとしてオブジェクトを使いたい場合にはどうすればいいでしょうか。

独自実装を行うためには、eql?メソッドとhashメソッドを定義してあげる必要があります。

Hashクラスは、キーオブジェクトのhashメソッドを呼び出しハッシュ値を取得してキー検索を行います。

"str".hash #=> -60268345

hashメソッドは同じキーとしたいオブジェクトが同じ値を返すように実装する必要があります。

逆に異なるキーオブジェクトであるのに同じ値を返してしまう時があります。これは衝突と呼ばれており、衝突を避けるためにeql?メソッドを使います。

実際に独自実装をする場合には内部データの何かしらに委譲してしまうのが簡単かと思います。今回はnameが同じであれば同じキーとする実装にしてみました。

class User
  attr_accessor :name, :age
  def initialize(name, age=0)
    @name = name
    @age = age
  end

  def ==(v)
    v.respond_to?(:name) && name == v.name
  end

  def hash
    name.hash
  end

  def eql?(other)
    name.eql?(other.name)
  end
end

===演算子

最後は===演算子です。

見慣れない演算子ですが、これはcase式の内部で使われている比較演算子です。

例えば、以下のcaseを見てみます

a = "str"

case a
when "ste"
  p "ste"
when 1
  p 1
else
  p "not match!"

これをif文で直すと次のようになります

a = "str"
if "ste" === a
  p "ste"
elsif 1 === a
  p 1
else
  p "not match!"
end

実は==演算子ではなく、===が使われているのです。

ここで注意ですが、if文に書き直した方を見てみると比較対象("ste"や1)が演算子の左に来ています。

というのも、===は比較対象それぞれのクラスで実装されているものを使うという意味なのです。

rubyでは比較演算子などの特別な演算子にはメソッド名の前にスペースを付けることができるので、1 === aというのは要するに1.===(a)と同じものを意味しています。

===はメソッド名です。

では、=====は何が異なるのでしょうか。

文字列クラスや数値クラスでは実は全く同じ実装になっています。

しかし、Regexpクラスでは実装が異なっています。

a = Regexp.new(/er/)
b = Regexp.new(/er/)

a == b #=> true
a === b #=> false
a === "user" #=> true

面白いことに、Regexpクラスでは同じ正規表現かどうかチェックするのではなく、右辺の文字列が正規表現にマッチするかどうかチェックします。

なので、caseでは次のようなことができます。

a = "user"
case a
when /or/
  p "○○or"
when /er/
  p "○○er"
else
  p "not match!!"
end

もしいくつかの解説書にあるようにcaseが==を使ったif文の単なる書き換えであればこれは説明がつかないことです。

大抵の場合は気を付ける必要はありませんが、caseが==ではなく、===を使っているというのは意識しておく必要がありそうです。

Effective Ruby

Effective Ruby

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング