反思異常、表達式和空
在 上一期文章 中,我介紹了 Java 下一代語言用來消除 Java 語言中華而不實的東西和復雜性的創新方式。在這一期 文章中,我將展示這些語言如何消除 Java 的一些瑕疵:異常、語句與表達式,以及圍繞 null 的邊緣情況。
表達式
Java 語言從 C 語言那裡繼承的一項傳承是區分編程語言 和編程表達式。Java 語句的示例包 括使用 if 或 while 的代碼行,以及使用 void 來聲明不會返回任何值的方法的代碼行。表達式(比如 1 + 2 )用於求取 某一個值。
這種區分在最早的編程語言中就已經開始,比如在 Fortran 中,這種區分基於硬件條件以及對編程語言 設計的初步了解。在許多語言中,它被保留為操作(語句)與求值(表達式)的指示器。但語言設計人員逐漸意識到,該語 言可以完全由表達式組成,在對結果不感興趣的時候忽略結果。事實上,所有函數式語言完全可以消除這種區分,僅使用表 達式。
Groovy 的 if 和 ?:
在 Java 下一代語言中,傳統的命令式語言(Groovy)和函數式語言(Clojure 和 Scala)之間的分離展示了向表達式的進化。Groovy 仍然包含語句,這些語句基於 Java 語法,但添加了更多的表達式 。而 Scala 和 Clojure 則完全使用表達式。
語句和表達式中包含的內容都為語言增添了語法上的笨拙。可以考慮 Groovy 中的 if 語句,它繼承自 Java。它有兩個版本,清單 1 對它們進行了比較,這兩個版本是用於執行判斷的 if 語 句,以及神秘的三元運算符 ?::
清單 1. Groovy 的兩個 if 語句
def x = 5 def y = 0 if (x % 2 == 0) y = x * 2 else y = x - 1 println y // 4 y = x % 2 == 0 ? (x *= 2) : (x -= 1) println y // 4
在 if 語句 清單 1 中,我必須將 x 的值設置為一個副作用 (side effect),因為 if 語句沒 有返回任何值。要執行判斷並同時進行賦值,必須使用三元賦值,如 清單 1 中的第二個賦值語句所示。
Scala 的 基於表達式的 if 語句
Scala 消除了對三元運算符的需求,允許 if 表達式對兩種情況都進行處理。您可以使用它 ,就像在 Java 代碼中使用 if 語句那樣(忽略返回值),或者在賦值語句中使用它,如清單 2 中所示:
清單 2. Scala 的基於表達式的 if 語句
val x = 5 val y = if (x % 2 == 0) x * 2 else x - 1 println(y)
Scala 和其他兩種 Java 下一代語言一樣,不要求方法中包含明確的 return 語句。因此,方法的最 後一行是返回值,強調了這些語言中的方法的基於表達式的特性。
當您在 Java 和 Groovy 代碼中進行操作和設置 值時,可以將每個響應封裝為一個代碼塊,如清單 3 中所示,並包含任何所需的副作用:
清單 3. Scala if + 副 作用
val z = if (x % 2 == 0) { println("centerisible by 2") x * 2 } else { println("not centerisible by 2; odd") x - 1 } println(z)
在 清單 3 中,除了返回新計算得出的值之外,我還為每種情況打印了一條狀態消息。代碼塊中的代 碼行的順序非常重要:代碼塊的最後一行表示符合條件的返回值。因此,當您使用基於表達式的 if 進行混合求值和具有副 作用時,必須非常小心。
Clojure 的表達式和副作用
Clojure 也完全由表達式組成,但它更進一步,從求值 代碼中區分出了副作用代碼。前兩個示例的 Clojure 版本是用一個 let 代碼塊表達的,在清單 4 中,這允許定義局部作 用域變量:
清單 4. Clojure 的基於表達式的 if 語句
(let [x 5 y (if (= 0 (rem x 2)) (* x 2) (- x 1))] (println y))
在 清單 4 中,我為 x 分配了一個值 5,然後使用 if 建立了表達式來計算兩個條件:(rem x2 ) 調用了 remainder 函數,類似於 Java % 操作符,並將結果與零值進行比較,在除以 2 時檢查零剩余值(zero remainder)。在 Clojure 的 if 表達式中,第一個參數是 condition,第二個參數是 true 分支,第三個參數是可選的 else 分支。if 表達式的結果被分配給 y,然後被打印出來。
Clojure 也允許對每個條件使用代碼塊(可以包含副 作用),但需要一個包裝器,比如 (do ...)。包裝器通過使用最後一行作為代碼塊的返回值,對代碼塊中的每個表達式 進行求值。清單 5 說明了如何對某個條件或副作用進行求值:
清單 5. Clojure 中的顯式副作用
(let [x 5 a (if (= 0 (rem x 2)) (do (println "centerisible by 2") (* x 2)) (do (println "not centerisible by 2; odd") (- x 1)))] (println a))
在 清單 5,我為 if 表達式的返回值分配了 a。對於每個條件,都創建了一個 (do ...) 包裝 器,並允許使用任意數量的語句。代碼塊的最後一行是 (do...) 代碼塊的返回值,這類似於 清單 3 中的 Scala 示例。請確保目標返回值是最後進行求值的。以這種方式使用 (do...) 代碼塊是如此之常見,以致於 Clojure 中的許多 結構(比如 (let []))已經包含隱式 (do ...) 代碼塊,這消除了許多情況下對它們的需求。
Java/Groovy 代 碼和 Scala/Clojure 代碼中的表達式的比較指示了編程語言中的總趨勢,即消除不必要的語句/表達式分歧。
異常
對於我而言,Java 編程中 “似乎最不錯的特性” 是已檢查出的異常以及廣播和(實施)異常意識(exception awareness) 的能力。事實上,這帶來了一場噩夢,它強迫用戶進行斷章取義的、不必要的異常處理(和誤操作)。
所有 Java 下一代語言都使用了已經內置於 JVM 中的異常機制,以及基於 Java 語法的語法,並對這些語法進行了 修改,以獲得它們自己的獨一無二的語法。這些語言都消除了已檢查出的異常,在執行 Java 交互操作期間遇到這些異常時 ,會將它們轉換成為 RuntimeExceptions。
Scala 對 Java 異常處理機制的轉換表現出了一些興趣,想在它 自己的基於表達式的世界中采用該機制。首先,應考慮到的事實是,異常可能是表達式的返回值,如清單 6 中所示:
清單 6. 異常是返回值
val quarter = if (n % 4 == 0) n / 4 else throw new RuntimeException("n must be quarterable")
在 清單 6 中,我分配了 n 值的 1/4 或一個異常 。如果觸發了異常,那麼返回值將沒有任何意義,因為在求取返回值之前,異常已經傳播開來。這種墨守成規的賦值看著似 乎有些奇怪,可以將 Scala 視為一種類型化的語言。Scala 異常類型不是一種數字類型,開發人員不熟悉這種類型 ,可將它視為 throw 表達式的返回值。Scala 以獨創的方式解決了這個問題,它使用特殊的 Nothing 類型作為 throw 的返回類型。Any 在 Scala 中位於繼承層次結構的頂部(類似於 Java 中的 Object),這意味著所有類都可以擴展 它。相反,Nothing 位於底部,它是其他所有類的自動子類。因此,編譯 清單 6 中的代碼是合法的:它要麼返回一個數字 ,要麼在設置返回值之前觸發異常。編譯器沒有報告錯誤,這是因為 Nothing 是 Int 的一個子類。
其次,finally 代碼塊在基於表達式的世界中有一些有趣的行為。Scala 的 finally 代碼塊的作用類似於其 他功能,但有一些與返回值有關的微妙行為。請考慮清單 7 中的代碼:
清單 7. Scala 的 finally 返回值
def testReturn(): Int = try { return 1 } finally { return -1 }
在 清單 7 中,總體返回值是 -1。finally 代碼塊的返回值“覆蓋”了從 try 語句的主體返回的值。這個令 人吃驚的結果僅出現在 finally 代碼塊包含顯式 return 語句時,隱式返回值被忽略,如清單 8 中所示:
清單 8. Scala 的隱式返回值
def testImplicitReturn(): Int = try { 1 } finally { -1 }
在 清單 8 中,函數的返回值是 1,這說明打算使用 finally 代碼塊作為清理副作用的地方,而不是將它用 作對表達式進行求解的地方。
Clojure 也是完全基於表達式的。(try ...) 的返回值總是以下兩者之一:
沒有異常的 try 代碼塊的最後一行
捕獲了異常的 catch 代碼塊的最後一行
清單 9 顯示了 Clojure 中用於 異常的語法:
清單 9. Clojure 的 (try...catch...finally) 代碼塊
(try (do-work) (do-more-work) (catch SomeException e (println "Caught" (.getMessage e)) "exception message") (finally (do-clean-up)))
在 清單 9 中,主路徑的返回值是來自 (do-more-work) 的返回值。
Java 下一代 語言汲取了 Java 異常機制的長處,擯棄了該機制的缺點。此外,盡管有些實現有所不同,但它們設法將這些異常整合到基 於表達式的透視圖中。
空
在 2009 年於 QCon London 召開的報告會議中,Tony Hoare 將他發明的“null” 概念稱為 ALGOL W(1965 年引入的一種實驗性的面向對象的語言),“十億美元的錯誤” 是由於編程語言中的 null 引用 所導致的所有問題帶來的。Java 語言自身也遇到了一些與 null 有關的邊緣情況,Java 下一代語言解決了這些問題。
例如,Java 編程中一個習慣用語用於防止在您試圖調用方法之前出現 NullPointerException:
if (obj != null) {
obj.someMethod();
}
Groovy 已經將這種模式封裝在安全導航 操作符 ?. 中。它自動 進行左側的 null 檢查,並嘗試僅在返回值為非 null 時進行方法調用;否則返回 null。
obj?.someMethod ();
def streetName = user?.address?.street
也可以采用嵌套方式調用安全導航操作符。
Groovy 的密 切相關的 Elvis 操作符 ?: 縮短了默認值中的 Java 三元運算符。例如,以下這些代碼行是等效的:
def name = user.name ? user.name : "Unknown" //traditional ternary operator usage
def name = user.name ?: "Unknown" // more-compact Elvis operator
當左側有一個值(通常是默認值)時,Elvis 操 作符會保護它,否則設置一個新值。Elvis 操作符是三元運算符的一個較短的、傾向於操作符的版本。
Scala 增強 了 null 的概念,並使它成為一個類(scala.Null),並附帶一個相關的類 scala.Nothing。Null 和 Nothing 都位於 Scala 類分層結構的底部。Null 是每個引用類的子類,而 Nothing 則是其他每種類型的子類。
Scala 提供了 null 和表達式的替代物,以指示是否缺少值。許多關於收集的 Scala 操作(比如 Map 上的 get)會返回一個 Option 實例,該 示例包含以下兩個部件之一(但絕不會兩個都包括):Some 或 None。清單 10 中的 REPL 交互顯示了一個示例:
清單 10. Scala 的 Option 返回值
scala> val designers=Map("Java" -> "Gosling", "c" -> "K&R", "Pascal" -> "Wirth") designers: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(Java -> Gosling, c -> K&R, Pascal -> Wirth) scala> designers get "c" res0: Option[java.lang.String] = Some(K&R) scala> designers get "Scala" res1: Option[java.lang.String] = None
請注意,在 清單 10 中,來自成功的 get 的返回值是一個 Option [java.lang.String] = Some(value),反之則是 None 中的一個失敗的查找結果。從收集值中獲得展開值 (unwrapping value) 的一項技術使用了模式匹配,模式匹配本身是一個表達式,它允許在一個簡潔的表達式中訪問和展開某個值:
println(designers get "Pascal" match { case Some(s) => s; case None => "? "})
Option 允許使用比單獨使用 null 更好的空表達式,尤其在為該表達式的使用提供語法支持時。
結束語
在這一期的文章中,我深入探討了 Java 語言的三個問題領域:表達式、異常和 null。每種 Java 下一代語 言都可以消除 Java 的瑕疵,但每種語言都有自己的慣用方式。表達式的出現改變了用於看似不相關的概念(比如異常)的 一些習慣用語和選項——進一步闡述了語言特性彼此之間是高度耦合的。
Java 開發人員有時會習慣性地認為繼承是 擴展行為的惟一方式。在下一期文章中,我將展示 Java 下一代語言如何提供許多更強大的替代。