https://mauricio.github.io/2011/05/30/ruby-basics-equality-operators-ruby.html
Ruby有很多相等操作符,有些在我们的应用中随处可见(比如常见的双等号——“==”),还有一些可能没那么常见(比如三等号——“===”),现在就让我们深入挖掘一下Ruby究竟是怎样进行对象间比较的。
相等意味着什么?
可能由于我过去Java的经验,我认为首先定义什么是“相等”很有意义。Ruby中的对象都有其标识符,可以通过调用object_id
方法轻易的查看。
some_string = 'some string'
=> "some string"
another_string = 'another_string'
=> "another_string"
[ some_string.object_id, another_string.object_id ]
=> [2164900860, 2164888440]
随着继续创建新对象,Ruby为每个对象分派object_id,你可以用它来区分各个对象。
matz = 'matz'
=> "matz"
matz_2 = 'matz'
=> "matz"
[ matz.object_id, matz_2.object_id ]
=> [2164840660, 2164818480]
以上两个对象的值完全相同,但是拥有各自不同object_id,所以Ruby并不认为这两个值是相同的。object_id可以视为标识对象的捷径,每个对象都可以用其跟其他对象进行对比。
两个代表相同字符的字符串应该是相等的,两个身份证号一样的人应该是同一个人。编程语言必须提供对应的手段来实现这种级别的比较,在Ruby中它们就是这些相等方法。
"=="——双等号
双等号方法应该实现对象的通用标识算法。这通常意味着应该对比对象属性,可不是去考虑它们在内存中是否是同一个对象。鉴于Ruby是动态类型语言,所以不要过于依赖类型,应该去依赖方法,不要去判断对象是否来自于某个类,而是检查它是否能够响应特定的方法:
class Meter
def initialize( value )
@value = value
end
def to_meters
@value.to_f
end
def ==( other )
if other.respond_to?( :to_meters )
self.to_meters == other.to_meters
end
end
end
首先验证对象是否有期待使用的方法而不是验证其类型,然后将两个对象进行对比。如果返回'false'则说明两个对象不相等。
“hash”方法
如果已经实现了"=="方法,那也应该实现"eql?"和"hash"方法,就算你从来没有见过这些方法在哪里被调用过。因为如果你的对象作为哈希键使用时,这些是哈希对象跟你的对象进行比较使用的方法。哈希需要快速判断键是否已经存在,所以要避免每个单独对象的比较。通过使用hash
方法的返回值将对象分组分类,然后再批量的使用eql?
对比对象。
搜寻哈希中的键值时,首先调用 键的hash
方法判定其属于哪一组,然后使用eql?
与同组内其他对象进行比较。就算是最坏的情况,我们也只需要和三个对象进行对比(哈希拥有九个对象),还是可以接受的。但是如果使用数组的话,最坏的情况下我们需要进行9次比较。
举个例子简单说明:
class StringWithoutHash
def initialize(text)
@text = text
end
def to_text
@text
end
def ==(other_value)
if other_value.respond_to?(:to_text)
self.to_text == other_value.to_text
end
end
end
这里已经正确实现了"=="方法,但是还没有实现"hash"和"eql?"。所以在哈希中无法判断其属于同一类型(不会被分配到同一组中进行比较)。
context 'without hash and eql? methods' do
it 'should be equal' do
@first = StringWithoutHash.new('first')
@second = StringWithoutHash.new('first')
@first.should == @second
end
it 'should add as two different keys in the hash' do
@texts = {}
10.times do
@texts[ StringWithoutHash.new('first') ] = 'one'
end
@texts.keys.size.should == 10
end
end
即使@firsh和@second代表的值完全相同,不过鉴于我们没有实现"hash"和"eql?"方法,在哈希中还是会生成两个键而不是一个。通用的做法是如果重载来"=="方法,那么也应该重载"hash"和"eql?"。实现"hash"时的一个基本规则是,如果两个对象是"=="的,那么它们应该生成完全相同的哈希值(可以在哈希对象中被找到),但是拥有相同哈希值的两个对象还是不同的(它们属于同一组但是并不属于同一对象)。
下例中的正确实现eql?
和hash
方法的StringWithHash是StringWithoutHash的子类(eql?
方法可以直接使用==
,我们甚至不用为它编写代码)
class StringWithHash < StringWithoutHash
def eql?( other )
self == other
end
def hash
@text.hash
end
end
对应的测试代码如下:
context 'with hash and eql? methods' do
it 'should be equal' do
@first = StringWithHash.new('first')
@second = StringWithHash.new('first')
@first.should == @second
end
it 'should add as a single key in the hash' do
@texts = {}
50.times do
@texts[ StringWithHash.new('first') ] = 'one'
end
@texts.keys.size.should == 1
end
end
因为我们正确实现了eql?
和 hash
方法,现在就算加五十次对象到哈希中,得到的哈希数量还是为1。
除非你清楚你在做什么(并且了解Ruby中的hash是怎么实现的),否则不要尝试去自己实现hash
方法,就像Number
和String
类做的那样,直接使用BasicObject
中的hash
方法就好了。如下示例:
class Meter
# all the other methods
def hash
self.to_meters.hash
end
def eql?( other )
self == other
end
end
在这里,我的hash
方法直接调用了Float
的hash
。没有什么特殊需求的话,你可以完全照搬我这里的实现。
“===”——三等号
三等号是个有意思的操作符,它散布在Ruby的源码中,但是大部分开发者在实际开发中却很少看到它。为什么会这样呢?因为它隐藏在一个常用的控制结构case/when
中。当使用case/when
时,实际上就是在使用===
操作符。也正是===
操作符令Ruby的case条件比C或者Java的更强大。
来看看下面的例子:
age = 19
case age
when 1..18
puts 'just out of college'
when 19..30
puts 'wild years'
when 31..40
puts 'i better find a job in a big corp'
else
puts 'retirement plan'
end
这段代码将调用===
并打印出'wild years'。看出来它是怎么工作的了么?以下是if
版本的实现:
if 1..18 === age
puts 'just out of college'
elsif 19..30 === age
puts 'wild years'
elsif 31..40 === age
puts 'i better find a job in a big corp'
else
puts 'retirement plan'
end
总的来说,case/when
语句只是简化并美化了===
和if
语句组合的操作。对于语言自身,三等号更多被用作“分组”操作。通过传入一个值来判定其分组。