天天看點

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

本節書摘來自華章出版社《effective ruby:改善ruby程式的48條建議》一書中的第2章,第2.7節,作者 [美]彼得 j.瓊斯(peter j. jones),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

看看下面的irb會話然後自問一下:為什麼方法equal?的傳回值和操作符“==”的不同呢?

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

事實上,有四種方式來檢查對象之間的等價性。有時各種不同的方法互相重疊地使用相同方式進行比較,但如你所見,并不總是這樣。基于不同的比較方法和對象調用,你能得到意外的結果。但這種情況不會再發生了,因為我們已經搞清楚了它們的差別。

似乎通過四種不同方式來比較對象的等價性有點過分了。對多數對象是這樣的,因為這四種方法最終做了相同的事情。但有些時候,這些等價方法常常存在少許(并非那麼少)差别。比如,很多表示數字的類在使用“==”操作符比較對象時會做類型的隐式轉換。

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

每當你定義一個類時,它都繼承了四個不同的等價性檢查方法。如果你想重載它們中的任何一個,了解它們的工作方式就非常重要。如果你計劃如第13條中讨論的那樣實作所有的比較操作符,那了解它們就更有用了。我們将從簡單的開始——你可能從來沒有注意過的一個方法。

你可能對于上述例子中equal?方法和“=”操作符的傳回結果不同感到驚訝。顯然,它們測試的東西是不同的。就目前而言我敢說equal?這個名字是一個誤用。它實際上并不是比較兩個對象的内容和值,而是檢查它們是否為同一個對象。也就是說,如果它們具有相同的object_id,其值才會為真。(從内部實作上講,equal?檢查兩個對象是否指向記憶體中同一個塊。)

即使上面兩個字元串具有相同的内容,但它們顯然不是同一個對象。它們隻是恰好具有相同字元的不同對象而已。每次ruby評估一個字元串時,會配置設定一個新的string對象,即使具有相同字元串的對象已經在記憶體中存在了。當然,這是你希望發生的。你肯定不希望在改變一個字元串的同時也意外地改變了程式中另一個具有相同内容的字元串。(如果你是想對字元串做寫時拷貝(copy-on-write),不隻有你想這麼做。ruby 2.1中提供了可以共享記憶體的不變字元串,即相同字元串,詳見第47條。)

關于equal?方法重要的一點是其值相等的那些對象的行為應該是完全一緻的。即,你不該在重載方法時給它不同的實作。因存在依賴這個方法的幾個類,如果改變了其實作,會以奇怪的方式破壞它們。如果你希望比較對象的值,equal?方法可能并不是合适的選擇。你也許會對“==”操作符更感興趣。

在比較對象時,“==”做了你想做的事,有時它做得好到讓你驚訝。每個類都可以重定義“==”,其慣例行為是:當兩個對象表示相同的值時傳回真值。這解釋了前面的字元串對比以及對1和1.0的值的判斷,順便說一下,1和1.0是由兩個不同的類fixnum和float表示的。這可能和你預想的等價性的比較是一緻的。

如果你沒有自定義“==”的實作,它就會繼承預設實作,其行為與方法equal?一樣。這可能不是很有用的行為,因為顯然有些對象即使沒有在相同的記憶體位置上存儲,它們在比較時也應該被認為是相等的。這時你需要使用“==”操作符來比較對象的内容。也許這隻是簡單地對對象屬性、記錄id的比較,或是将比較行為委派給另一個對象。無論哪種方式,“==”操作符應該都比equal?方法高明。不過你要抵制直接定義“==”的誘惑。

第13條解釋了如何通過定義“<=>”操作符來毫無代價地定義“==”(以及其他操作符)并将其嵌入到子產品comparable中。如果你對定義排序操作符如“>”感興趣,你應該遵循那一條中的建議。

下一個要講的等價性方法的命名很不明确并有些難以了解。不過這個方法很重要。eql?方法在hash類中被廣泛用于比較對象的鍵。你肯定不想在hash中将一個鍵插入多次,是以定義合理的eql?方法隻是完成了一半。我們等會将學習另一半。

eql?方法的預設比較行為和equal?一樣,并且它可能比你想得更加嚴格。如果你定義一個類并使用這個類的執行個體如哈希的鍵,同時又沒有重載其預設實作,你将很快感到驚訝。由于eql?方法嚴格基于object_id比較對象,你很可能最終擁有一個很大的哈希,而這并不是你預期的。比如,假設有如下這個color類。

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

如果我們建立兩個color類并賦予相同的參數值,随後将其用于哈希的鍵,讓我們看看會發生什麼:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

即使不知道eql?方法會做什麼,你大概也會預期最終的哈希隻存在一個鍵,而非兩個。要将哈希中的這兩個鍵合并成一個,我們需要在定義eql?方法的同時講講另一個非常重要的方法hash。當hash類将對象用作鍵的時候,需要決定将對象存在資料結構的什麼地方。它通過調用對象上的hash方法來實作這個功能。當兩個對象表示相同鍵時它們的hash方法應該傳回相同的值。但是不同的對象在調用hash方法時也可以傳回相同的值。也就是說,它們并不需要一定是唯一的。當兩個對象的hash方法傳回了相同的值,會引發沖突,這将通過繼續比較eql?方法的值來确定其是否具有相同的鍵。是以,如果我們希望這兩個相似的color對象表示相同的哈希鍵,我們需要同時實作hash方法和eql?方法。對于這個簡單的類,我們将手工委派這兩個方法到@name屬性上:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

多數時候,你可能希望像第13條描述的那樣實作“<=>”操作符,然後簡單地用eql?來作為“==”的别名。那麼如果它隻是被作為相等操作符的别名,我們為什麼還需要這個方法呢?沒錯,這确實是個問題。讓我們通過回顧“==”操作符在對數字類所做的類型轉換來回答這個問題吧。再來看看這些類的“==”和eql?方法的差別:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

使用“==”操作符時認為1和1.0相等,而使用eql?方式時認為它們不等可能是合理的,是以這就是為什麼它們都會存在。當你定義自己的類時,你需要決定這個類的對象如何使用哈希鍵互相比較。如果你選擇寬松地實作“==”操作符,你需要嚴格地定義eql?方法,否則,用eql?作為“==”的别名。之前我說過但這裡有必要重申:記住,如果你沒有自行定義eql?方法,它将使用預設實作,也就是與equal?方法相同的行為。

最後,讓我們看看你始終在使用的操作符(你甚至沒有意識到它)——case equality操作符。這個操作符寫作三個等号(“===”),但常常通過case表達式間接使用它。自己來看:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

有兩種不同的方式來使用case表達式。我們關心的這一種是當你為case關鍵字提供一個表達式時ruby選擇的(上面的指令變量)。case使用“===”操作符來比較被指派的表達式與每個when語句的等價性。如果你移除文法糖并揭示出其下的if表達式,可能更容易看出發生了什麼:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

注意case關鍵字之後的表達式總是出現在“===”的右邊。這非常重要。在ruby中,左操作數是消息的接收者,右操作數是方法調用的唯一參數。這也意味着在ruby中,操作符不是必然可換的,因為它們的行為是由左操作數決定的。同時考慮這兩點,意味着你能夠用普通方法調用來使用任何操作符:

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

case equality操作符在這裡能做些什麼呢?好吧,需要知道的一件重要的事是哪個對象會作為接收者。當你知道了when語句所接收的表達式會作為“===”操作符的左操作數(也就是接收者)後,你就明白了目前操作符的實作方式。ruby核心類對case equality操作符有一些有用的變形。

“===”的預設實作很枯燥,它隻是将計算對象傳給“==”。這是在你自己的類中将會繼承的版本。不過當你注意一些類比如regexp時,事情就變得有趣起來了。regexp類定義了“===”操作符,如果作為字元串的參數和接收者(正規表達式)比對,那麼傳回值為真。你得将正規表達式作為左操作數;否則,你将不能使用regexp類中的“===”實作。

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

類和子產品中也定義了類方法版本的“===”操作符。它們共享了一個通用實作,如果右操作數是左操作數的一個執行個體,那麼其值為真。這幾乎是對接收者和參數轉化過的is_a?方法的操作符版本。這使我們能将類和子產品名作為when語句的參數。了解如果沒有case文法會怎樣工作是非常好的事情。思考is_a?和“===”的相似性。

《Effective Ruby:改善Ruby程式的48條建議》一第12條:了解等價的不同用法

你可能不用直接自定義“===”操作符,因為object的預設實作已經足夠了,你的類可以通過繼承來使用。不過,如果你希望你的類或對象在被用作case表達式中的when語句的參數時擁有特殊的行為,你得知道去重載哪個操作符。僅需記住,哪個操作數該作為接收者,哪個該作為參數。

要點回顧

絕不要重載equal?方法。該方法的預期行為是,嚴格比較兩個對象,僅當它們同時指向記憶體中同一對象時其值為真(即,當它們具有相同的object_id時)。

hash類在沖突檢測時使用eql?方法來比較鍵對象。預設實作可能和你的想象不同。遵循第13條的建議之後再使用别名eql?來替代“==”書寫更合理的hash方法。

使用“==”操作符來測試兩個對象是否表示相同的值。有些類比如表示數字的類會有一個粗糙的等号操作符進行類型轉換。

case表達式使用“===”操作符來測試每個when語句的值。左操作數是when的參數,右操作數是case的參數。