Source From Here
Preface
在 變數 中談過,在 Ruby 中,== 常用來比較兩個物件的實質內容是否相同。如果想知道兩個變數是否參考同一物件,除了使用 object_id 得知之外,通常還可以使用 equal? 方法。相等比較還可以使用 eql? 方法,這個方法通常會檢查變數是否參考同一實例,若否則比較物件是否為同一類別的實例,若是則比較實值是否相同。
物件相等性
簡單地說,equal? 測試是否為相同實例,== 測試實值是否相同,eql? 相當於先作 equal? 要作的事,再測試了是否為同一類別實例,最後進行 == 要作的事,不過要注意,eql? 預設並不呼叫 equal? 或 ==。Object上定義的相等比較還有個 ===,通常若 === 兩邊都是實例,預設實作會比較兩個變數是否參考同一實例,如果不是,會再呼叫 ==。如果左邊是類別而右且是實例,=== 比較實例是否由該類別所生成。使用 case...when...else 時,就是使用 === 作為依據(因為 === 會呼叫 ==,所以實際上可以僅定義 == 來決定case..when..else的比對依據)。
實際上,如果你定義類別時,沒有重新定義 ==、equal?、eql?、=== 方法,則定義預設繼承自 Object,若要定義類別時需要定義相等性,必須依需求自行定義 ==、equal?、eql?、=== 方法,而不是依賴 Object 的預設實作。
以下討論定義物件實質相等性時要考量的一些要素。例如,你可能如下定義了==方法:
如果你這麼測試:
看來似乎沒錯,p1 與 p2 座標都是同一點,所以實際上指的是相同座標,但是如果你這麼測試:
Set 是集合,相同的資料不會重複收集,不過上例中,Set 顯然不認為 p1 與 p2 是相同資料,無論是從 include? 或最後集合的長度,都看得出來這樣的結果。
事實上在許多場合,例如將物件加入 Set 時,會同時利用 eql? 與 hash 來判斷是否加入的是(實質上)相同的物件:
以 Set 為例,會先使用 hash 得出該將物件放至哪個雜湊桶(Hash buckets)中,如果雜 湊桶有物件,再進一步使用 eql? 確定相等性,從而確定 Set 中不會有重複的物件。以下是定義了 eql? 與 hash 的 Point 版本:
一個重要的觀念是,定義 eql? 與 hash 時,最好 別使用狀態會改變的資料成員。你可能會想,以這個例子來說,點會移動,如果移動了就不是相同的點了,不是嗎?若 x、y 是個允許會變動的成 員,那麼就會發生這個情況:
明 明是記憶體中同一個物件,但置入集合後,最後跟我說不包括 p1?這是因為,你改變了 x,算出來的 hash 也就改變了,使用 include? 嘗試比對時,會看看新算出來的雜湊桶中是不是有物件,而根本不是在置入 p1 的雜湊桶中尋找,結果就是 false了。
對 Point 應用於如 Set 的場合而言,x、y 最好是不可變動的。例如:
暫且忽略 hash。來看看在實作 eql? 時要遵守的約定(
取自java.lang.Object的 equals() 說明 ):
目前定義的 Point,其 eql? 方法滿足以上幾個約定(你可以自行寫程式測試)。現在考慮繼承的情況,你要定義 3D 的點:
這看來似乎沒什麼問題,3D 的點要再比較 z 座標是沒錯。不過來測試一下:
結 果該是 true 或 false 需要討論一下。3D 的點與 2D 的點是否相等呢?假設你考慮的是點投射在xy平面上是否相等,那
p1.eql?(p2) 為 true 就可以接受,在此假設之下,再來看 p2.eql?(p1) 為false,這違反 eql? 對稱性的對稱性合約。如果你要滿足對稱性,則 要作個修改:
再次運行上面的測試,就可以得到都是 true 的結果,但如果是這個:
p2 等於 p1,p1 等於 p3,但 p2 不等於 p3,這違反傳遞性合約。問題點在於,2D 的點並沒有z軸資訊,無論如何也沒辦法滿足傳遞性了。
一般來說,對於不同的類別實例,會將之視為不同,基本上你可以這麼設計:
在繼承的情況下,若 eql? 兩旁運算元有一個是子類別實例,則會使用子類別的 eql? 版本進行比對。在上面的定義之下,直接將 2D 與 3D 的點視作不同的類型,這避免了2D 點與 3D 點(
父、子類別)進行比較時,無法符合對稱性、傳遞性合約的問題。
不過在以下這種需求時,這樣的定義也許不符合你的需求:
之後會看到,Ruby 中的類別都是
Class 實例,上面的程式片段中,p2 是繼承 Point 的匿名類別建構出來,在程式中某處又打算測試看看 set 中是否含有相同座標的點,但結果並不是顯示 true,這是因為你嚴格地在 eql? 中檢查了實例的類別名稱。
你可以將定義改為以下:
在 eql? 中,你不僅檢查傳入的實例是否為
Point,也反過來讓傳入的實例取得 self 的型態進行測試(這是 Visitor 模式 的實現)。而在 Point3D 中:
如果 p1 是
Point 物件,而 p2 是 Point3D 物件,p1.eql?(p2) 時,由於傳入的實例可以取得 self 的型態進行測試,p2 反過來測試 p1 是不是 Point3D,結果不是,所以 eql? 傳回 false,利用這個方式,讓有具體名稱的子類別實例,不會與父類別實例有相等成立的可能性。如果是直接繼承 Point 類別的匿名類別物件,則直接繼承 canEqual? 方法,由於匿名類別物件還是一種 Point 實例,因此 eql? 的結果會是true。
一個測試的結果如下:
Supplement
* stackoverflow - What's the difference between equal?, eql?, ===, and ==?
* Stackoverflow - Ruby: kind_of? vs. instance_of? vs. is_a?
Preface
在 變數 中談過,在 Ruby 中,== 常用來比較兩個物件的實質內容是否相同。如果想知道兩個變數是否參考同一物件,除了使用 object_id 得知之外,通常還可以使用 equal? 方法。相等比較還可以使用 eql? 方法,這個方法通常會檢查變數是否參考同一實例,若否則比較物件是否為同一類別的實例,若是則比較實值是否相同。
物件相等性
簡單地說,equal? 測試是否為相同實例,== 測試實值是否相同,eql? 相當於先作 equal? 要作的事,再測試了是否為同一類別實例,最後進行 == 要作的事,不過要注意,eql? 預設並不呼叫 equal? 或 ==。Object上定義的相等比較還有個 ===,通常若 === 兩邊都是實例,預設實作會比較兩個變數是否參考同一實例,如果不是,會再呼叫 ==。如果左邊是類別而右且是實例,=== 比較實例是否由該類別所生成。使用 case...when...else 時,就是使用 === 作為依據(因為 === 會呼叫 ==,所以實際上可以僅定義 == 來決定case..when..else的比對依據)。
實際上,如果你定義類別時,沒有重新定義 ==、equal?、eql?、=== 方法,則定義預設繼承自 Object,若要定義類別時需要定義相等性,必須依需求自行定義 ==、equal?、eql?、=== 方法,而不是依賴 Object 的預設實作。
以下討論定義物件實質相等性時要考量的一些要素。例如,你可能如下定義了==方法:
- class Point
- attr_accessor :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- def ==(that)
- self.x == that.x && self.y == that.y
- end
- end
看來似乎沒錯,p1 與 p2 座標都是同一點,所以實際上指的是相同座標,但是如果你這麼測試:
Set 是集合,相同的資料不會重複收集,不過上例中,Set 顯然不認為 p1 與 p2 是相同資料,無論是從 include? 或最後集合的長度,都看得出來這樣的結果。
事實上在許多場合,例如將物件加入 Set 時,會同時利用 eql? 與 hash 來判斷是否加入的是(實質上)相同的物件:
以 Set 為例,會先使用 hash 得出該將物件放至哪個雜湊桶(Hash buckets)中,如果雜 湊桶有物件,再進一步使用 eql? 確定相等性,從而確定 Set 中不會有重複的物件。以下是定義了 eql? 與 hash 的 Point 版本:
- class Point
- attr_accessor :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- def ==(that)
- self.x == that.x && self.y == that.y
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point)
- return self == that
- end
- return false
- end
- def hash
- 41 * (41 + self.x) + self.y
- end
- end
- require "set"
- p1 = Point.new(2, 1)
- p2 = Point.new(2, 1)
- set = Set.new
- set << p1
- puts set.include?(p2) # true
- puts set.size # 1
- set << p2
- puts set.size # 1
- require "set"
- p1 = Point.new(2, 1)
- set = Set.new
- set << p1
- puts set.include?(p1) # true
- p1.x = 3
- puts set.include?(p1) # false
對 Point 應用於如 Set 的場合而言,x、y 最好是不可變動的。例如:
- class Point
- attr_reader :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- def ==(that)
- self.x == that.x && self.y == that.y
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point)
- return self == that
- end
- return false
- end
- def hash
- 41 * (41 + self.x) + self.y
- end
- end
目前定義的 Point,其 eql? 方法滿足以上幾個約定(你可以自行寫程式測試)。現在考慮繼承的情況,你要定義 3D 的點:
- require "Point2"
- class Point3D < Point
- attr_reader :z
- def initialize(x, y, z)
- super(x, y)
- @z = z
- end
- def ==(that)
- super(that) && self.z == that.z
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point3D)
- return self == that
- end
- return false
- end
- end
- p1 = Point.new(2, 3)
- p2 = Point3D.new(2, 3, 4)
- puts p1.eql?(p2) # true
- puts p2.eql?(p1) # false
- class Point3D < Point
- attr_reader :z
- def initialize(x, y, z)
- super(x, y)
- @z = z
- end
- def ==(that)
- super(that) && self.z == that.z
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point3D)
- return self == that
- end
- if that.is_a?(Point)
- return that == self
- end
- return false
- end
- end
- p1 = Point.new(2, 3)
- p2 = Point3D.new(2, 3, 4)
- p3 = Point3D.new(2, 3, 5)
- puts p2.eql?(p1) # true
- puts p1.eql?(p3) # true
- puts p2.eql?(p3) # false
一般來說,對於不同的類別實例,會將之視為不同,基本上你可以這麼設計:
- class Point
- attr_reader :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- def ==(that)
- self.x == that.x && self.y == that.y
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point)
- return clz_eql?(that) && self == that
- end
- return false
- end
- def hash
- 41 * (41 + @x) + @y
- end
- def clz_eql?(that)
- self.class == that.class
- end
- end
- class Point3D < Point
- attr_reader :z
- def initialize(x, y, z)
- super(x, y)
- @z = z
- end
- def ==(that)
- super(that) && self.z == that.z
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point3D)
- return clz_eql?(that) && self == that
- end
- return false
- end
- end
- p1 = Point.new(2, 3)
- p2 = Point3D.new(2, 3, 5)
- p3 = Point3D.new(2, 3, 5)
- puts p2.eql?(p1) # false
- puts p1.eql?(p3) # false
- puts p2.eql?(p3) # true
- puts p3.eql?(p2) # true
不過在以下這種需求時,這樣的定義也許不符合你的需求:
- require "set"
- p1 = Point.new(2, 1)
- p2 = Class.new(Point) {
- def to_s
- "(#{@x}, #{@y})"
- end
- }.new(2, 1)
- set = Set.new
- set << p1
- puts set.include? p1 # 顯示 true
- puts set.include? p2 # 顯示 false,但你想顯示 true
你可以將定義改為以下:
- class Point
- attr_reader :x, :y
- def initialize(x, y)
- @x = x
- @y = y
- end
- def ==(that)
- self.x == that.x && self.y == that.y
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point)
- return that.canEqual?(self) && self == that
- end
- return false
- end
- def hash
- 41 * (41 + @x) + @y
- end
- def canEqual?(that)
- that.is_a?(Point)
- end
- end
- class Point3D < Point
- attr_reader :z
- def initialize(x, y, z)
- super(x, y)
- @z = z
- end
- def ==(that)
- super(that) && self.z == that.z
- end
- def eql?(that)
- if self.equal? that
- return true
- end
- if that.is_a?(Point3D)
- return that.canEquals(self) && self == that
- end
- return false
- end
- def canEqual?(that)
- that.is_a? Point3D
- end
- end
一個測試的結果如下:
- require "set"
- p1 = Point.new(2, 1)
- p2 = Class.new(Point) {
- def to_s
- "(#{@x}, #{@y})"
- end
- }.new(2, 1)
- p3 = Point3D.new(2, 1, 3)
- set = Set.new
- set << p1
- puts set.include? p1 # true
- puts set.include? p2 # true
- puts set.include? p3 # false
Supplement
* stackoverflow - What's the difference between equal?, eql?, ===, and ==?
* Stackoverflow - Ruby: kind_of? vs. instance_of? vs. is_a?
沒有留言:
張貼留言