更新於 2024/04/27閱讀時間約 8 分鐘

關於 Swift & Java 中 Optional 的設計

此為過去的舊文,2014 年 7 月 6 日初次發表於 logdown。

近幾年電腦的運算效能已經好到一個程式語言的抽象程度遠比執行效率重要的層級,所以最近超多種程式語言冒出來(有種好累的感覺),特別是 Domain Specific Language,不過今天討論的還是 Genernal Purpose Language,只是在語法的抽象程度都過去的語言要好。

WWDC 2014 後就開始看《The Swift Programming Language》,看到 optionsals時,總覺得這好像看過耶,原來是在當初研究 Java 的 Stream API (參閱《Java 8 初探 - Stream》) 時,就看過 Java 8 內建相同的概念,但概念相同,實作卻不全然一樣,Java 的 Optional API level 的支援,Swift 則是從 language level 做支援。但如果真要說誰抄誰就很難說了 (ScalaOption 或是 Groovy 的 safe navigation operator 都是相似的的設計),畢竟最近的幾個新程式語言都從 functional programming language 借了很多特色,幾乎都支援 Lambda 就是一個明顯的例子。

不管是 Scala、Java 或是 Swift,概念上,Optioanl 是一個容器,裡面可能有值也可能沒有值,但是這個容器本身絕對不是 null (Java) 或 nil (Swift),因此在操作這個容器時,是絕對不會拋出NullPointerException (說是這麼說,但 Java 確實可以回傳空的 Optional,只能靠開發者堅守慣例才能讓這件事成真),但如果無視裡面是否有值就硬要取值還是會拋出 NoSuchElementException (Java) 或 Runtime error (Swift)。但是多了一層 Optional 是否真的能提高抽象程度呢?畢竟『是否有值』這件事還是得判斷,難道用了 Optional 就可以省去什麼麻煩嗎?首先看 Java 使用 Optional 後,使用上的差異吧!

以 Java 來說,後者搭配新的 Lambda expression 確實看起來賞心悅目許多。接著看 Swift 的例子,Swift的 if 和 Java一樣,只接受能產生 boolean 為結果的述句 (expression),所以 if possibleName 這個判斷式在解讀上和 C/C++ 不同 (C/C++ 的 if 是判斷述句的結果是否為非 0),但 Swift 是對 possibleName 這個 optional 物件先進行 evaluaton,若結果為 true,表示該物件是有值的,才執行大括弧裡的程式片段。除此之外,Swift 還有一種 optional binding 機制,即 if let name = possibleName,這一行的解讀為:先對 possibleName 物件進行evaluation,若結果為 true,則將 possibleName 所代表的物件指派給 name,因此在大括弧中 name物件保證是非 nil 可以安全存取的。


就上述的例子,是否有覺得抽象程度提高呢?或許再看二個例子吧,假設在使用某個沒有 API 文件的函式庫時,有 optioanl 和沒 optional 哪個版本能比較清楚知道解碼這個函式可能回傳一個不存在的物件呢?我想這應該很明顯,有 optional 的版本應該清楚很多,所以對我來說 optional 的引入,第一個好處是在做 API 設計時,可以提供一個很明確的回傳值定義,而不是透過文件的方式解釋回傳值可能不存在的情況。


剛才提到 Swift 對 optional 是 language level 的支援,除了 if 會自動對 optional 物件進行 evaluation和提供 optional binding 外,和 Groovy 一樣提供 optional chaining。假設 Person 物件有個可能不存在的 residence 屬性,代表其居住地,型別為 ResidenceResidenceAddress 紀錄地址,同樣可能不存在,Address 有個 street 屬性紀錄街名,同樣可能不存在,所以想透過 person 物件存取居住地的地址街名時,除了用 optional binding 層層解開外,Swift 提供 optional chaining:person.residence?.address?.street,只要在這串存取中任何一個屬性是不存在的,if 就會得到 false,也就不會執行指定的區塊,程式看起來較清爽簡潔許多。


那 Java 如何呢?Java 對 Optional 的支援大多是以 API 的形式存在,以剛剛的例子,若不想檢查 null,如下所示,需要搭配 Stream API 來使用,map(Function) 透過傳入的 Function 物件將 Optional<Person> 依序換轉 (想像成取得property) 成 Optional<Residence>Optional<Address>Optional<String>,最後用ifPresent(Consumer) 印出結果,就簡潔度來說 optional binding 確實簡潔多了。就抽象程度來說,用 Stream API 還真的需要一點想像力才能寫出這樣的程式碼,所以對我來說 optional chaining 的抽象度還是比較高一點。


整體來說,不論是 Swift 或是 Java,使用 Optional 來設計 API (注意,如果最後的例子中 getter 的回傳值是 Optional,那要用 flatMap(Function) 取代 map(Function),不然會拿到類似 Optional<Optional<Residence>> 的結果)應該都能提供更清楚的語義:該值可能不存在。

只是 Swift 以 language level 支援 Optional 確實比用 API level 支援的 Java 要簡潔和更具可讀性。不過,Java 是一個歷史悠久 (1995至今) 的語言,很難對 language 本身做出太大幅度的改變,反之,Swift 是一個全新的語言,從一開始的設計就將許多好的語言特性加入,確實讓人驚豔。


延伸閱讀


2024 補充

10 年過去,Java 已經敲敲升級到 Java 21,但在處理 null 上,仍然沒太多改善,但不難理解,對一個已經有廣大生態系的語言,以語言層級的方式支援 Optional,確實有點動搖國本的感覺,例如,真的類似 Swift 等語言,用 ? 代表一個 Optional 物件,假設既有的函數庫中,一個函式回傳一個物件,因為已經編譯成 JAR 檔了,原作者沒空處理,在新版本裡使用這個函式,編譯器應該視回傳值為 Optional 還是非 Optional?要保證安全,應該要視為 Optional,因為舊版的 Java 確實允許回傳空值,這時編譯器會報錯 (或是警告),光處理這些錯誤或警告,就是一件辛苦的差事。若視為非 Optional,等到執行時真的是 null,恐怕也不適合。但如果真要選,我會選前者,並將這類問題視為警告,讓開發者慢慢朝向新特性邁進,晚到遲到總比不到好。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.