Objectの比較メソッドを実装する。
Object#eql?などObjectの比較を行うAPIを実装してみました。オブジェクトの型とすべての属性が完全に一致する場合、"同一"とみなします。たいていのモデルクラスはこの仕様でOKかと。
実装
リファレンスによると以下のようなルールになっているようなので、これに従ってmoduleとして実装。
- eql?,==,===はオブジェクトごとにカスタマイズ可能となっているのでこれをオーバーライドする。
- ==はifで、===はcase, eql?はハッシュでの同一性判定に使われるらしい。
- eql?を変更したらhashも変更する必要がある。これはJavaと同じ。
- equals?はオーバーライドしてはいけない。
# #== オブジェクトの比較メソッドの実装。 # #Object#eal?(other), Object#hash, Object#==(other) の基本実装です。 #オブジェクトの型とすべての属性が完全に一致する場合、"同一"とみなします。 # module Equals def ==(other) _eql?(other) { |a,b| a == b } end def ===(other) _eql?(other) { |a,b| a === b } end def eql?(other) _eql?(other) { |a,b| a.eql? b } end def hash hash = 0 values.each {|v| hash = v.hash + 31 * hash } return hash end protected def values values = [] values << self.class instance_variables.each { |name| values << instance_variable_get(name) } return values end private def _eql?(other, &block) return false if other == nil return false unless other.is_a? Equals a = values b = other.values return false if a.length != b.length a.length.times{|i| return false unless block.call( a[i], b[i] ) } return true end end
使い方
includeして利用します。
class TestModel include Equals end
valuesをオーバーライドすれば、比較する属性をカスタマイズすることもできます。
class TestModel include Equals def initialize( title, text ) @title = title @text = text end def values # タイトルのみ比較する。 # title属性のみを保持していれば型も問わない。 [@title] end end
サンプル
サンプルです。テストケースにしてみた。以下のテストは一応通ります。
#!/usr/bin/ruby require "runit/testcase" require "runit/cui/testrunner" require "equals" class EqualsTest < RUNIT::TestCase # テスト用モデル class TestModel include Equals def initialize( title, text ) @title = title @text = text end end # テスト用モデルの派生クラス class TestModel2 < TestModel; end # 比較値をカスタマイズしたモデルクラス class TestModel3 include Equals def initialize( title, text ) @title = title @text = text end def values # タイトルのみ比較する。 # title属性のみを保持していれば型も問わない。 [@title] end end # テスト用モデル3の派生クラス class TestModel4 < TestModel3; end # テスト用モデルを持つモデル class TestModel5 include Equals def initialize( title, text, title2, text2 ) @m1 = TestModel.new title, text @m3 = TestModel3.new title2, text2 end end def test_equals # 同じ型で同じ属性を持つ場合同一と見なされる a = TestModel.new("a","a") b = TestModel.new("a","a") assert_equals a, b # 属性が違う場合、eql?はtrueにならない。 b = TestModel.new("b","a") assert_not_equals a, b b = TestModel.new("a","b") assert_not_equals a, b # 型が違う場合も違うと見なされる。 a = TestModel.new("a","a") b = TestModel2.new("a","a") assert_not_equals a, b # valuesをカスタマイズしたもの。 # 型を問わない。また、titleしか評価しない。 a = TestModel3.new("a","a") b = TestModel3.new("a","b") assert_equals_ignore_type a, b b = TestModel3.new("b","a") assert_not_equals a, b b = TestModel4.new("a","b") assert_equals_ignore_type a, b # Equalsをincludeしていないオブジェクトも同一と見なされることはない assert_not_equals a, 1 assert_not_equals a, "foo" assert_not_equals a, Object.new # Equalsを実装したモデルを持つモデル # 再帰的に評価される。 a = TestModel5.new("a","a","a","a") b = TestModel5.new("a","a","a","a") assert_equals a, b b = TestModel5.new("b","a","a","a") assert_not_equals a, b b = TestModel5.new("a","b","a","a") assert_not_equals a, b b = TestModel5.new("a","a","b","a") assert_not_equals a, b b = TestModel5.new("a","a","a","b") assert_equals a, b end def assert_equals( a, b ) assert a.eql?( b ) assert a == b assert !(a === b) # Clas#===(other) がfalseを返すのでfalseになる。 assert b.eql?( a ) assert b == a assert !(b === a) assert_equal a.hash, b.hash assert !a.equal?( b ) assert !b.equal?( a ) end def assert_not_equals( a, b ) assert !a.eql?( b ) assert !(a == b) assert !(a === b) assert !b.eql?( a ) assert !(b == a) assert !(b === a) assert_not_equal a.hash, b.hash assert !a.equal?( b ) assert !b.equal?( a ) end def assert_equals_ignore_type( a, b ) assert a.eql?( b ) assert a == b assert a === b assert b.eql?( a ) assert b == a assert b === a assert_equal a.hash, b.hash assert !a.equal?( b ) assert !b.equal?( a ) end end
修正 (2007-11-29)
==が実行された場合、属性も==を使って評価する方がよいな、と思って修正。Class#===がtrueを返さないので===での比較時にfalseになってしまうのがちょっとアレだけど、まぁいいか。ちなみにClass#===の実装はModlueの===を引き継いでおり、以下の動作となっているようです。(Rubyリファレンスマニュアル - Modlue - self === objより。)
obj が self と Object#kind_of? の関係がある時、真になります。
あと、Equalsをincludeしたオブジェクトを持つオブジェクトのテストがなかったので追加しておきました。