計算一個數的絕對值是非常基礎的操作,幾乎所有主流的編程語言都內置了相應的函數或方法。
在 PHP、Python、SQL 等語言中,直接調用 abs() 函數即可,例如 abs(-1)。到了 Java、C# 這類面向對象的語言中,abs() 通常是 Math 類的靜態方法,調用時要加上前綴 Math.,即 Math.abs(-1)。
Go 語言就要稍微麻煩一點了,因為 math 包中的 Abs() 函數僅支持 float64 類型的參數,如果要計算整數的絕對值,就不得不先手寫一個 func abs(x int)。常刷 LeetCode 的同學對此一定深有體會——在寫實際算法之前,先要定義好 min()、max()、abs() 等輔助函數,都快形成肌肉記憶了吧。
本文的主角 Ruby 則更加特別,竟支持 -1.abs() 這樣的寫法。也就是説,整數(字面量)可以直接“點”一個 abs() 方法,這與大多數語言的風格都不同。
puts -1.abs()
可這也算不上 Ruby 的“奇葩”之處吧。因為在 Rust、C#、Kotlin 等語言中,類似 -1.abs() 的寫法並不稀奇。這些語言都提供了某種機制,使整數可以直接調用方法。
例如,在 C# 中,可以通過 擴展方法 (extension methods)實現類似的效果:
using System;
public static class IntExtensions {
public static int Abs(this int value) => Math.Abs(value);
}
class Program {
static void Main() {
Console.WriteLine(-1.Abs());
}
}
Rust 則乾脆直接支持 Ruby 的這種寫法,只不過要先將 -1 通過 _f64 後綴轉換為 float64 類型:
fn main() {
println!("{}", -1_f64.abs());
}
那為什麼還説 Ruby “奇葩” 呢?請思考一下,-1.abs() 的結果應該是多少?
- 是 -1 的絕對值嗎,即
(-1).abs() == 1? - 還是 1 的絕對值的相反數,即
-(1.abs()) == -1?
在 Rust、C#、Kotlin、Swift 等一眾語言中,-1.abs() 的結果都是 -1。而在 Ruby 中,-1.abs() 的結果是 1。表面上看,Ruby 反倒是“奇葩”的那個,但如果從直覺出發,Ruby 的行為比 Rust、C# 這些語言更貼近人們的理解吧。
從左往右讀,-1.abs() 就是“負 1 的絕對值”,結果自然應該是 1。而 Rust 等語言的計算順序卻如同 “先計算 1.abs(),然後才想起來前面還一個負號呢,再取相反數”,得到 -1。
那 Ruby 到底是”眾人皆醉我獨醒“,還是“旁人清醒獨我迷”呢?
其實,“奇葩”的 Ruby 沒有錯,Rust、C# 陣營也沒有錯。這並不是對錯的問題,而是不同語言(的設計者)對於 運算符優先級 的不同選擇: -(負號)和 .(方法調用)誰的優先級更高?
我們都熟悉 “先乘除,後加減”的數學運算優先級,但在編程語言的規範和設計中,. 和 - 誰先執行並沒有公認的標準,語言的設計者會根據自己的理念做出選擇。
Ruby 的創造者 松本行弘(Matz) 曾提到:“Ruby 的設計原則之一是讓編程更快樂。”
相比於 因為不瞭解 . 和 - 的優先級而踩坑,排查半天,本以為發現了什麼“驚天 Bug”而沾沾自喜,最後卻一盆涼水澆下來,發現只是少加了個括號——還不如 Ruby 這種更符合直覺的“奇葩”行為更讓人快樂。
畢竟,直覺和可讀性也是編程體驗的重要部分。Ruby 選擇的方式可能小眾,但卻更符合人們的思維習慣。
如果習慣了 Rust、C# 陣營的解析方式,可能會覺得 Ruby “奇葩”;但對於 Ruby 的開發者而言,可能又會覺得 -1.abs() == -1 才不合理。
沒有絕對的對錯,只有不同的選擇。