本節書摘來自華章出版社《effective ruby:改善ruby程式的48條建議》一書中的第2章,第2.8節,作者 [美]彼得 j.瓊斯(peter j. jones),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
在第12條中提到了四種測試對象相等性的方法。如果你對對象的排序和比較有興趣,那麼你就需要進一步定義其他的比較操作符了。與等價操作符不同的是,類并沒有從其他比較操作符中繼承預設實作。還好,ruby提供了一種簡便的方式來實作它,這一點我們會在稍後讨論。
首先,讓我們做一件有意思的事情,就是實作一個具有特殊序列的類。作為程式員,我們已經習慣了奇怪的版本号的定義,是以它們并不會給我們造成太多的困擾。但對外行來說就完全不同了。如何比較“10.10.3”和“10.9.8”這兩個版本号呢?很明顯,如果我們使用詞典序來比較它們那就錯了。為了得到正确的答案,你需要分别比較它們的每個部分。這就是接下來我們在version類中要做的事情。
為了更清楚地表現,我們暫且處理僅含三個部分的版本号(就像前面描述的那樣)。第一部分是主版本,接下來是次版本,最後是修訂版本。同時,為了更加專注于版本号的比較,我們僅處理那些符合格式要求的版本号。這樣,将一個用來描述版本的字元串解析成獨立的幾個部分的工作就變得簡單多了。

我想再次強調,一般說來類不會自動繼承比較操作符,但有一個例外。這一點我們會在之後讨論,現在你要做的就是為這個類定義一個比較操作符,也就是“<=>”操作符。這個特别的操作符其實繼承自object類,但是這個繼承的實作卻是不完整的。讓我們看看如果試圖對一組version類的對象進行排序時會發生什麼:
這對我們來說并沒有什麼幫助。要怪就怪“<=>”操作符的預設實作好了。它隻考慮了兩個對象是否相同(使用equal?和“===”操作符),卻沒有按我們的需要完整地進行比較。如果兩個被比較的對象不相同就傳回nil,進而告訴sort方法這個比較是非法的。但也還好,畢竟你不能對一個通用的實作抱有太大的期望,是以咱們還是自己動手實作一個吧。
完整地實作比較操作符需要兩步。最難的一部分是編寫一個合理的“<=>”操作符(通俗的說法是“太空艙”操作符)。要記住在ruby語言中,二進制操作符最終會被轉換成方法調用的形式,左操作數對應着方法的接收者,右操作數對應着方法第一個也是唯一的那個參數。當編寫一個比較操作符時,通常的做法是将參數命名為“other”,因為它代表着要比較的另一個對象。
由于“<=>”操作符的傳回值非常靈活,是以它可以被看成全能的比較操作符。它可以傳回如下四種情況:
當接收者和參數的比較無意義時,比較操作符理應傳回nil。因為參數可能是另一個類的執行個體,甚至可以是nil。對于某些類來說,在比較之前将參數轉換成正确的類型是很有用的,但通常情況下更好的做法是,當接收者和要比較的參數不是同一個類的執行個體時就直接傳回nil。我們要實作的version類就将這樣做。
如果接收者比參數小,則傳回-1。換句話說,如果使用“<”操作符比較的結果是真,那麼使用“<=>”比較的結果就應該是一個負值。
如果接收者比參數大,則傳回1。這就要用“>”操作符來解釋了。如果使用“>”操作符比較的結果是真,那麼使用“<=>”比較的結果就應該是一個正值。
如果接收者與參數相等,傳回0。換句話說,隻有當“<=>”傳回值為0時,使用“==”比較的結果才是真。
我們想讓version類中的比較操作符和數值類中的比較操作符有一緻的行為。事實上,我們可以像數值類一樣實作我們的版本。你可以看到,它的實作遵循了前面提及的規則:
當編寫“<=>”時,通常的做法是将比較方法代理給對象的執行個體變量。version類中的三個變量都是fixnum類的執行個體,這意味着它們都實作了可用的“<=>”操作符。這大大簡化了我們的工作。為了比較版本号,我們需要考慮接收者(左操作數)中的執行個體變量以及那些參數中的執行個體變量(右操作數)從主版本到修訂版本的順序。一旦接收者中有一個變量與參數中對應的變量不相符,我們就可以停止比較。換句話說,如果要比較的兩個版本的主版本号不相符,那麼無需比較次版本号或修訂版本号就已經可以知道誰大誰小。但是如果主版本号相同,就需要比較次版本号。以此類推,當次版本号也相同時就需要比較修訂版本号。當所有部分和另一個都相同時,我們的比較操作符應該傳回0以表示兩個version對象的等價性。而其他情況下,隻需使用“<=>”操作符對第一組不比對的變量進行操作,并将其結果傳回。思考version類中比較操作符的實作:
每個部分都會分别被比較,并将其比較的結果存放于一個數組中。我們需要做的僅僅是在這個數組中尋找第一個非零元素(第一對不相同的部分)。如果所有部分都相同,那麼detect方法就會傳回nil,而此時比較操作符傳回0。現在,我們就可以對一組version對象進行排序了。
為了完整地實作version對象的比較功能,我們還需添加一段代碼。除了排序之外,我們想讓這些對象能夠使用諸如“>”、“>=”這樣的操作符。事實上,這五個操作符構成了完整的排序操作符,它們是:“<”、“<=”、“==”、“>”和“>=”。你如果知道我們無需自己手動實作它們一定會很高興吧。我們要做的隻是引入一個名為comparable的子產品。
就這麼簡單。現在我們可以使用所有的比較操作符以及一個額外的helper方法:
當使用comparable子產品時,還有些因素需要考慮一下。首先,對于某些類你可能想實作自己的“==”操作符,是以這比使用comparable子產品中的方法間接一些。在第12條中有一個很好的例子,其在做比較之前對數值類型進行了轉換。如果你想這樣做,你需要編寫自己的等價操作符或者改變使“<=>”操作符傳回0的條件。具體如何選擇,應由你希望其他的比較運算符表現出什麼樣的行為來決定。
如果你想讓“>=”、“<=”和“==”傳回一緻的結果,那麼應該改變“<=>”計算相等性的方式。如果無需保持一緻,簡單地重載“==”就可以了,使其相較于其他比較運算符不那麼嚴格。但是對于多數類,你可能希望使其所有運算符都互相一緻。
最後,如果你希望類的執行個體能被作為哈希表的鍵來使用,你需要再做兩件事情。首先,使“eql?”方法作為“==”操作符的别名。因為“eql?”的預設實作和“equal?”相同,如果不重新實作它會使類中的“<=>”操作符變得沒有意義。而别名則使得哈希類使用由comparable子產品定義的“==”運算符來進行比較。
你還需要定義一個傳回fixnum類對象的哈希方法。為了使哈希類達到最好的性能,應該確定不同的對象傳回不同的哈希值。下面是一個version類的簡單的示範實作(編寫一個優化的哈希方法不在本書的讨論範圍之内)。
要點回顧
通過定義“<=>”操作符和引入comparable子產品實作對象的排序。
如果左操作數不能與右操作數進行比較,“<=>”操作符應該傳回nil。
如果要實作類的“<=>”運算符,應該考慮将eql?方法設定為“==”操作符的别名,特别是當你希望該類的所有執行個體可以被用來作為哈希鍵的時候,就應該重載哈希方法。