程式扎記: [ 文章收集 ] Ruby Equality And Object Comparison

標籤

2014年11月7日 星期五

[ 文章收集 ] Ruby Equality And Object Comparison

Source From Here 
Preface 
Just like other object oriented languages, Ruby gives an object ways to find out if it is equal to, greater or less than another object. Object comparison is extremely important, not only do we tend to often explicitly compare objects to each other e.g.: 
  1. string1 = "abc"  
  2. if "abc" == string1  
  3.   puts 'they are equal'  
  4. end  
but objects are frequently compared and tested for equality ‘behind the scenes’, by core and library classes (i.e. ordering of objects in collections etc.). So, lets not waste any time and jump straight in. 

Testing Objects For Equality 
Ruby has three main equality test methods, ==eql? and equal?. These methods normally live in the Object class and since all other Ruby classes inherit from Object, they automatically gain access to these three methods. Inside the Object class all there methods do exactly the same thing, they test if two objects are exactly the same object. That is to say, both objects must have the same object id. We can easily demonstrate this e.g.: 
  1. string1 = "abc"  
  2. class MyObject  
  3. end  
  4. object1 = MyObject.new  
  5. object2 = object1  
  6. object3 = MyObject.new  
  7.   
  8. puts "Object 1 is == to object 2: #{object1 == object2}"  
  9. puts "Object 1 is eql? to object 2: #{object1.eql? object2}"  
  10. puts "Object 1 is equal? to object 2: #{object1.equal? object2}"  
  11. puts "Object 1 is == to object 3: #{object1 == object3}"  
  12. puts "Object 1 is eql? to object 3: #{object1.eql? object3}"  
  13. puts "Object 1 is equal? to object 3: #{object1.equal? object3}"  
The output is: 
Object 1 is == to object 2: true
Object 1 is eql? to object 2: true
Object 1 is equal? to object 2: true
Object 1 is == to object 3: false
Object 1 is eql? to object 3: false
Object 1 is equal? to object 3: false

As you can see, when two variables are referencing the same object, calling any of the three equality methods will tell us that the two object are equal. As soon as two variables reference different objects (even if everything about the objects is otherwise identical), all three methods will tell us that the objects are unequal. 

Of course, it is not very useful to have 3 different methods that do the same thing. Usually with Ruby we will tend to leave the equal? method alone to always give us the ability to find out if two objects are exactly the same. The eql? and == methods however are open to be redefined in any way we like. It might still seem a little strange to have two methods that do the same thing. I will come back to this later. In the meantime lets look at the == method (or operator if you prefer). 

You can redefine the == method to give your objects custom behavior when it comes to equality testing. Lets do this for a Sock class: 
  1. class Sock  
  2.   attr_reader :size  
  3.   def initialize size  
  4.     @size = size  
  5.   end  
  6.   
  7.   def ==(another_sock)  
  8.     self.size == another_sock.size  
  9.   end  
  10. end  
You may now compare your Sock objects in an intuitive fashion based on the size. More than that, by defining the == method on your object, you also get the != method for free which allows you to test your objects for inequality, very handy e.g.: 
  1. sock1 = Sock.new(10)  
  2. sock2 = Sock.new(11)  
  3. sock3 = Sock.new(10)  
  4.   
  5. puts "Are sock1 and sock2 equal? #{sock1 == sock2}"  
  6. puts "Are sock1 and sock3 equal? #{sock1 == sock3}"  
  7. puts "Are sock1 and sock2 NOT equal? #{sock1 != sock2}"  
Which produces: 
Are sock1 and sock2 equal? false
Are sock1 and sock3 equal? true
Are sock1 and sock2 NOT equal? true

But, what if you want to compare two objects to find out which one is greater? 

Comparing Ruby Objects 
The == is not only an equality method, it is also part of a family of comparison methods that also include, >, <, >=, <=, and !=. Whenever you need to be able to compare your object and not just test for equality, redefining the == method is no longer enough and you must take a different approach. 

In order to give your object the ability to be compared to other objects, you need to do two things: 
* Mix in the the Ruby Comparable module into your class
* Define a method called <=>, this is known as the comparison method or ‘spaceship method’

Mixing in the module is pretty simple, but how do you define the <=> method? This is also fairly intuitive, if you’re familiar with any other object oriented language. If you’ve ever used the Comparable interface in Java for example, you’ll be right at home. Essentially, the method must return –1 when you think your current object is smaller than the one you’re comparing against. If you think it is larger you need to return +1, otherwise you return zero (the objects are equal). Let’s have a look at an example: 
  1. class Sock  
  2.   include Comparable  
  3.   attr_reader :size  
  4.   def initialize size  
  5.     @size = size  
  6.   end  
  7.   
  8.   def <=>(another_sock)  
  9.     if self.size < another_sock.size  
  10.       -1  
  11.     elsif self.size > another_sock.size  
  12.       1  
  13.     else  
  14.       0  
  15.     end  
  16.   end  
  17. end  
Defining the spaceship method allows your object to use the whole suite of comparison methods (==, >, <, >=, <=, and !=) e.g.: 
  1. sock1 = Sock.new(10)  
  2. sock2 = Sock.new(11)  
  3. sock3 = Sock.new(10)  
  4.   
  5. puts "Are sock1 and sock3 equal? #{sock1 == sock3}"  
  6. puts "Are sock1 and sock2 NOT equal? #{sock1 != sock2}"  
  7. puts "Is sock1 > sock3? #{sock1 > sock3}"  
  8. puts "Is sock1 < sock2? #{sock1 < sock2}"  
  9. puts "Is sock1 >= sock3? #{sock1 >= sock3}"  
  10. puts "Is sock1 <= sock2? #{sock1 <= sock2}"  
Which produces: 
Are sock1 and sock3 equal? true
Are sock1 and sock2 NOT equal? true
Is sock1 > sock3? false
Is sock1 < sock2? true
Is sock1 >= sock3? true
Is sock1 <= sock2? true

f you ask me, this is a pretty decent amount of functionality to get out of defining just one method. It is interesting to note that you can easily define all those comparison methods separately, instead of defining the spaceship, if you were so inclined. 

Also, notice that you automatically get the == method by defining the spaceship, so you don't need to provide separate equality behavior for your object any more. Although the Ruby built-in String class does define the == method separately from that provided by the <=> method, so you're still able to customize the behavior of comparison methods even if you've already defined the <=> method. 

Eql? vs == 
I did say I would come back to this one :). As I mentioned in core Ruby classes the equal? method is used to to find out if two objects have the same object id (are the same object). The == is normally used to find out if two objects have the same value (as you would expect). The third method, eql? is normally used to test if two object have the same value as well as the same type. For example: 
  1. puts "Does integer == to float: #{25 == 25.0}"  
  2. puts "Does integer eql? to float: #{25.eql? 25.0}"  
This gives: 
Does integer == to float: true
Does integer eql? to float: false

You are of course welcome to reproduce similar behavior in your classes when you define your equality methods. However it does seem a little redundant to me. If you need two object to be equal then you will probably want them to be the same type in the first place (the above example with integers and floats is the only exception I can think of), in which case you would make sure they were the same type even in the == method. If you don't really care about type and just care about the behavior of the objects (which is entirely possible considering Ruby's dynamic nature, with duck typing and all), then you probably wouldn't test for type equality in either the == method or the eql? method. Basically the eql? becomes redundant. 

There is only one scenario that I can see where the eql? method might come in handy. If you have to defined the <=> method, therefore giving your object an implicit ==method, it may no longer make sense to redefine == separately (although as I mentioned above, nothing stops you from doing so). But if for some reason you need to keep the<=> equality behavior but still need even more custom equality behavior, you can always use the eql? method for this purpose. I can't really envisage a situation where you might want to do this, but the capability is certainly there. 

This leaves the eql? method as a rather useless one. However, it is defined and used by many of the core Ruby classes as well as the library classes so you need to be aware of it and know when it is likely to come into play. However I can't really see you ever needing to define it for your own classes given that you have equal? and == already. 

Supplement 
Stackoverflow - What is the Ruby <=> (spaceship) operator? 
Basically instead of returning 1 (true) or 0 (false) depending on whether the arguments are equal or unequal, the spaceship operator will return 1, 0, or −1 depending on the value of the left argument relative to the right argument.
  1. a <=> b :=  
  2.   if a < b then return -1  
  3.   if a = b then return  0  
  4.   if a > b then return  1  
  5.   if a and b are not comparable then return nil  

[ Ruby Gossip ] Basic : 類別 - 物件相等性

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!