[zz]Kotlin 和 Checked Exception

Kotlin 和 Checked Exception

最近 JetBrains 的 Kotlin 語言忽然成了熱門話題。國內小編們傳言說,Kotlin 取代了 Java,成為了 Android 的“欽定語言”,很多人聽了之后熱血沸騰。初學者們也開始注意到 Kotlin,問出各種“傻問題”,很“功利”的問題,比如“現在學 Kotlin 是不是太早了一點?” 結果引起一些 Kotlin 老鳥們的鄙視。當然也有人來信,請求我評價 Kotlin。
對于這種評價語言的請求,我一般都不予理睬的。作為一個專業的語言研究者,我的職責不應該是去評價別人設計的語言。然而瀏覽了 Kotlin 的文檔之后,我發現 Kotlin 的設計者誤解了一個重要的問題——關于是否需要 checked exception。對于這個話題我已經思考了很久,覺得有必要分享一下我對此的看法,避免誤解的傳播,所以我還是決定寫一篇文章。
可以說我這篇文章針對的是 checked exception,而不是 Kotlin,因為同樣的問題也存在于 C# 和其它一些語言。
冷靜一下
在進入主題之前,我想先糾正一些人的誤解,讓他們冷靜下來。我們首先應該搞清楚的是,Kotlin 并不是像有些國內媒體傳言的那樣,要“取代 Java 成為 Android 的官方語言”。準確的說,Kotlin 只是得到了 Android 的“官方支持”,所以你可以用 Kotlin 開發 Android 程序,而不需要繞過很多限制。可以說 Kotlin 跟 Java 一樣,都是 Android 的官方語言,但 Kotlin 不會取代 Java,它們是一種并存關系。
這里我不得不批評一下有些國內技術媒體,他們似乎很喜歡片面報道和歪曲夸大事實,把一個平常的事情吹得天翻地覆。如果你看看國外媒體對 Kotlin 的報道,就會發現他們用詞的迥然不同:
Google’s Java-centric Android mobile development platform is adding the Kotlin language as an officially supported development language, and will include it in the Android Studio 3.0 IDE.

譯文:Google 的以 Java 為核心的 Android 移動開發平臺,加入了 Kotlin 作為官方支持的開發語言。它會被包含到 Android Studio 3.0 IDE 里面。

看明白了嗎?不是“取代了 Java”,而只是給了大家另一個“選擇”。我發現國內的技術小編們似乎很喜歡把“選擇”歪曲成“取代”。前段時間這些小編們也有類似的謠傳,說斯坦福大學把入門編程課的語言“換成了 JavaScript”,而其實別人只是另外“增加”了一門課,使用 JavaScript 作為主要編程語言,原來以 Java 為主的入門課并沒有被去掉。我希望大家在看到此類報道的時候多長個心眼,要分清楚“選擇”和“取代”,不要盲目的相信一個事物會立即取代另一個。
Android 顯然不可能拋棄 Java 而擁抱 Kotlin。畢竟現有的 Android 代碼絕大部分都是 Java 寫的,絕大部分程序員都在用 Java。很多人都知道 Java 的好處,所以他們不會愿意換用一個新的,未經時間考驗的語言。所以雖然 Kotlin 在 Android 上得到了和 Java 平起平坐的地位,想要程序員們從 Java 轉到 Kotlin,卻不是一件容易的事情。
我不明白為什么每當出現一個 JVM 的語言,就有人歡呼雀躍的,希望它會取代 Java,似乎這些人跟 Java 有什么深仇大恨。他們已經為很多新語言熱血沸騰過了,不是嗎?Scala,Clojure…… 一個個都像中國古代的農民起義一樣,煽動一批人起來造反,而其實自己都不知道自己在干什么。Kotlin 的主頁也把“drastically reduce the amount of boilerplate code”作為了自己的一大特色,仿佛是在暗示大家 Java 有很多“boilerplate code”。
如果你經過理性的分析,就會發現 Java 并不是那么的討厭。正好相反,Java 的有些設計看起來“繁復多余”,實際上卻是經過深思熟慮的決定。Java 的設計者知道有些地方可以省略,卻故意把它做成多余的。不理解語言“可用性”的人,往往盲目地以為簡短就是好,多寫幾個字就是丑陋不優雅,其實不是那樣的。關于 Java 的良好設計,你可以參考我之前的文章《為 Java 說句公道話》。另外在《對 Rust 語言的分析》里面,我也提到一些容易被誤解的語言可用性問題。我希望這些文章對人們有所幫助,避免他們因為偏執而扔掉好的東西。
實際上我很早以前就發現了 Kotlin,看過它的文檔,當時并沒有引起我很大的興趣。現在它忽然火了起來,我再次瀏覽它的新版文檔,卻發現自己還是會繼續使用 Java 或者 C++。雖然我覺得 Kotlin 比起 Java 在某些小地方設計相對優雅,一致性稍好一些,然而我并沒有發現它可以讓我興奮到愿意丟掉 Java 的地步。實際上 Kotlin 的好些小改進,我在設計自己語言的時候都已經想到了,然而我并不覺得它們可以成為人們換用一個新語言的理由。
Checked Exception(CE)的重要性
有幾個我覺得很重要的,具有突破性的語言特性,Kotlin 并沒有實現。另外我還發現一個很重要的 Java 特性,被 Kotlin 的設計者給盲目拋棄了。這就是我今天要講的主題:checked exception。我不知道這個術語有什么標準的中文翻譯,為了避免引起定義混亂,下文我就把它簡稱為“CE”好了。
先來科普一下 CE 到底是什么吧。Java 要求你必須在函數的類型里面聲明它可能拋出的異常。比如,你的函數如果是這樣:
void foo(string filename) throws FileNotFoundException{ if (...) { throw new FileNotFoundException(); } ...}

Java 要求你必須在函數頭部寫上“throws FileNotFoundException”,否則它就不能編譯。這個聲明表示函數在某些情況下,會拋出 FileNotFoundException 這個異常。由于編譯器看到了這個聲明,它會嚴格檢查你對 foo 函數的用法。在調用 foo 的時候,你必須使用 try-catch 處理這個異常,或者在調用的函數頭部也聲明 “throws FileNotFoundException”,把這個異常傳遞給上一層調用者。
try{ foo("blah");} catch (FileNotFoundException e){ ...}

這種對異常的聲明和檢查,叫做“checked exception”。很多語言(包括 C++,C#,JavaScript,Python……)都有異常機制,但它們不要求你在函數的類型里面聲明可能出現的異常類型,也不使用靜態類型系統對異常的處理進行檢查和驗證。我們說這些語言里面有“exception”,卻沒有“checked exception”。
理解了 CE 這個概念,下面我們來談正事:Kotlin 和 C# 對 CE 的誤解。
Kotlin 的文檔明確的說明,它不支持類似 Java 的 checked exception(CE),指出 CE 的缺點是“繁瑣”,并且列舉了幾個普通程序員心目中“大牛”的文章,想以此來證明為什么 Java 的 CE 是一個錯誤,為什么它不解決問題,卻帶來了麻煩。這些人包括了 Bruce Eckel 和 C# 的設計者 Anders Hejlsberg
很早的時候我就看過 Hejlsberg 的這些言論。他的話看似有道理,然而通過自己編程和設計語言的實際經驗,我發現他并沒有抓住問題的關鍵。他的論述里有好幾處邏輯錯誤,一些自相矛盾,還有一些盲目的臆斷,所以這些言論并沒能說服我。正好相反,實在的項目經驗告訴我,CE 是 C# 缺少的一項重要特性,沒有了 CE 會帶來相當麻煩的后果。在微軟寫 C# 的時候,我已經深刻體會到了缺少 CE 所帶來的困擾。現在我就來講一下,CE 為什么是很重要的語言特性,然后講一下為什么 Hejlsberg 對它的批評是站不住腳的。
首先,寫 C# 代碼時最讓我頭痛的事情之一,就是 C# 沒有 CE。每調用一個函數(不管是標準庫函數,第三方庫函數,還是隊友寫的函數,甚至我自己寫的函數),我都會疑惑這個函數是否會拋出異常。由于 C# 的函數類型上不需要標記它可能拋出的異常,為了確保一個函數不會拋出異常,你就需要檢查這個函數的源代碼,以及它調用的那些函數的源代碼……
也就是說,你必須檢查這個函數的整個“調用樹”的代碼,才能確信這個函數不會拋出異常。這樣的調用樹可以是非常大的。說白了,這就是在用人工對代碼進行“全局靜態分析”,遍歷整個調用樹。這不但費時費力,看得你眼花繚亂,還容易漏掉出錯。顯然讓人做這種事情是不現實的,所以絕大部分時候,程序員都不能確信這個函數調用不會出現異常。
在這種疑慮的情況下,你就不得不做最壞的打算,你就得把代碼寫成:
try{ foo();} catch (Exception){ ...}

注意到了嗎,這也就是你寫 Java 代碼時,能寫出的最糟糕的異常處理代碼!因為不知道 foo 函數里面會有什么異常出現,所以你的 catch 語句里面也不知道該做什么。大部分人只能在里面放一條 log,記錄異常的發生。這是一種非常糟糕的寫法,不但繁復,而且可能掩蓋運行時錯誤。有時候你發現有些語句莫名其妙沒有執行,折騰好久才發現是因為某個地方拋出了異常,所以跳到了這種 catch 的地方,然后被忽略了。如果你忘了寫 catch (Exception),那么你的代碼可能運行了一段時間之后當掉,因為忽然出現一個測試時沒出現過的異常……
所以對于 C# 這樣沒有 CE 的語言,很多時候你必須莫名其妙這樣寫,這種做法也就是我在微軟的 C# 代碼里經常看到的。問原作者為什么那里要包一層 try-catch,答曰:“因為之前這地方出現了某種異常,所以加了個 try-catch,然后就忘了當時出現的是什么異常,具體是哪一條語句會出現異常,總之那一塊代碼會出現異常……” 如此寫代碼,自己心虛,看的人也糊涂,軟件質量又如何保證?
那么 Java 呢?因為 Java 有 CE,所以當你看到一個函數沒有聲明異常,就可以放心的省掉 try-catch。所以這個 C# 的問題,自然而然就被避免了,你不需要在很多地方疑惑是否需要寫 try-catch。Java 編譯器的靜態類型檢查會告訴你,在什么地方必須寫 try-catch,或者加上 throws 聲明。如果你用 IntelliJ,把光標放到 catch 語句上面,可能拋出那種異常的語句就會被加亮。C# 代碼就不可能得到這樣的幫助。


CE 看起來有點費事,似乎只是為了“讓編譯器開心”,然而這其實是每個程序員必須理解的事情。出錯處理并不是 Java 所特有的東西,就算你用 C 語言,也會遇到本質一樣的問題。使用任何語言都無法逃脫這個問題,所以必須把它想清楚。在《編程的智慧》一文中,我已經講述了如何正確的進行出錯處理。如果你濫用 CE,當然會有不好的后果,然而如果你使用得當,就會起到事半功倍,提高代碼可靠性的效果。
Java 的 CE 其實對應著一種強大的邏輯概念,一種根本性的語言特性,它叫做“union type”。這個特性只存在于 Typed Racket 等一兩個不怎么流行的語言里。Union type 也存在于 PySonar 類型推導和 Yin 語言里面。你可以把 Java 的 CE 看成是對 union type 的一種不完美的,丑陋的實現。雖然實現丑陋,寫法麻煩,CE 卻仍然有著 union type 的基本功能。如果使用得當,union type 不但會讓代碼的出錯處理無懈可擊,還可以完美的解決 null 指針等頭痛的問題。通過實際使用 Java 的 CE 和 Typed Racket 的 union type 來構建復雜項目,我很確信 CE 的可行性和它帶來的好處。
現在我來講一下為什么 Hejlsberg 對于 CE 的批評是站不住腳的。他的第一個錯誤,俗話說就是“人笨怪刀鈍”。他把程序員對于出錯處理的無知,不謹慎和誤用,怪罪在 CE 這個無辜的語言特性身上。他的話翻譯過來就是:“因為大部分程序員都很傻,沒有經過嚴格的訓練,不小心又懶惰,所以沒法正確使用 CE。所以這個特性不好,是沒用的!”
他的論據里面充滿了這樣的語言:
“大部分程序員不會處理這些 throws 聲明的異常,所以他們就給自己的每個函數都加上 throws Exception。這使得 Java 的 CE 完全失效。”
“大部分程序員根本不在乎這異常是什么,所以他們在程序的最上層加上 catch (Exception),捕獲所有的異常。”
“有些人的函數最后拋出 80 多種不同的異常,以至于使用者不知道該怎么辦。”……

注意到了嗎,這種給每個函數加上 throws Exception
或者 catch (Exception)
的做法,也就是我在《編程的智慧》里面指出的經典錯誤做法。要讓 CE 可以起到良好的作用,你必須避免這樣的用法,你必須知道自己在干什么,必須知道被調用的函數拋出的 exception 是什么含義,必須思考如何正確的處理它們。
另外 CE 就像 union type 一樣,如果你不小心分析,不假思索就拋出異常,就會遇到他提到的“拋出 80 多種異常”的情況。出現這種情況往往是因為程序員沒有仔細思考,沒有處理本來該自己處理的異常,而只是簡單的把下層的異常加到自己函數類型里面。在多層調用之后,你就會發現最上面的函數累積起很多種異常,讓調用者不知所措,只好傳遞這些異常,造成惡性循環。終于有人煩得不行,把它改成了“throws Exception”。
我在使用 Typed Racket 的 union type 時也遇到了類似的問題,但只要你嚴格檢查被調用函數的異常,盡量不讓它們傳播,嚴格限制自己拋出的異常數目,縮小可能出現的異常范圍,這種情況是可以避免的。CE 和 union type 強迫你仔細的思考,理順這些東西之后,你就會發現代碼變得非常縝密而優雅。其實就算你寫 C 代碼或者 JavaScript,這些問題是同樣存在的,只不過這些語言沒有強迫你去思考,所以很多時候問題被稀里糊涂掩蓋了起來,直到很長時間之后才暴露出來,不可救藥。
所以可以說,這些問題來自于程序員自己,而不是 CE 本身。CE 只提供了一種機制,至于程序員怎么使用它,是他們自己的職責。再好的特性被濫用,也會產生糟糕的結果。Hejlsberg 對這些問題使用了站不住腳的理論。如果你假設程序員都是糊里糊涂寫代碼,那么你可以得出無比驚人的結論:所有用于防止錯誤的語言特性都是沒用的!因為總有人可以懶到不理解這些特性的用法,所以他總是可以濫用它們,繞過它們,寫出錯誤百出的代碼,所以靜態類型沒用,CE 沒用,…… 有這些特性的語言都是垃圾,大家都寫 PHP 就行了 ;)
Hejlsberg 把這些不理解 CE 用法,懶惰,濫用它的人作為依據,以至于得出 CE 是沒用的特性,以至于不把它放到 C# 里面。由于某些人會誤用 CE,結果就讓真正理解它的人也不能用它。最后所有人都退化到最笨的情況,大家都只好寫 catch (Exception)
。在 Java 里,至少有少數人知道應該怎么做,在 C# 里,所有人都被迫退化成最差的 Java 程序員 ;)
另外,Hejlsberg 還指出 C# 代碼里沒有被 catch 的異常,應該可以用“靜態分析”檢查出來。可以看出來,他并不理解這種靜態檢查是什么規模的問題。要能用靜態分析發現 C# 代碼里被忽略的異常,你必須進行“全局分析”,也就是說為了知道一個函數是否會拋出異常,你不能只看這個函數。你必須分析這個函數的代碼,它調用的代碼,它調用的代碼調用的代碼…… 所以你需要分析超乎想象的代碼量,而且很多時候你沒有源代碼。所以對于大型的項目,這顯然是不現實的。
相比之下,Java 要求你對異常進行 throws 顯式聲明,實質上把這個全局分析問題分解成了一個個模塊化(modular)的小問題。每個函數作者完成其中的一部分,調用它的人完成另外一部分。大家合力幫助編譯器,高效的完成靜態檢查,防止漏掉異常處理,避免不必要的 try-catch。實際上,像 Exceptional 一類的 C# 靜態檢查工具,會要求你在注釋里寫出可能拋出的異常,這樣它才能發現被忽略的異常。所以 Exceptional 其實重新發明了 Java 的 CE,只不過 throws 聲明被寫成了一個注釋而已。
說到 C#,其實它還有另外一個特別討厭的設計錯誤,引起了很多不必要的麻煩。感興趣的人可以看看我這篇文章:《可惡的 C# IDisposable 接口》。這個問題浪費了整個團隊兩個月之久的時間。所以我覺得作為 C# 的設計者,Hejlsberg 的思維局限性相當大。我們應該小心的分析和論證這些人的言論,不應該把他們作為權威而盲目接受,以至于讓一個優秀的語言特性被誤解,不能進入到新的語言里。
結論?
所以我對 Kotlin 是什么“結論”呢?我沒有結論,這篇文章就像我所有的看法一樣,僅供參考。顯然 Kotlin 有的地方做得比 Java 好,所以它不會因為沒有 CE 而完全失去意義。我不想打擊人們對新事物的興趣,我甚至鼓勵有時間的人去試試看。
我知道很多人希望我給他們一個結論,到底是用一個語言,還是不用它,這樣他們就不用糾結了,然而我并不想給出一個結論。一來是因為我不想讓人感覺我在“控制”他們,如何看待一個東西是他們的自由,是否采用一個東西是他們自己的決定。二來是因為我還沒有時間和機會,去用 Kotlin 來做實際的項目。另外,我早就厭倦了試用新的語言,如果一個大眾化的語言沒有特別討厭,不可原諒的設計失誤,我是不會輕易換用新語言的。我寧愿讓其他人做我的小白鼠,去試用這些新語言。到后來我有空了,再去看看他們的成功或者失敗經歷 :P
所以對我個人而言,我至少現在不會去用 Kotlin,但我并不想讓其他人也跟我一樣。因為 Java,C++ 和 C 已經能滿足我的需求,它們相當穩定,而且我對它們已經很熟悉,所以我為什么要花精力去學一個新的語言,去折騰不成熟的工具,放下我真正感興趣的算法和數據結構等問題呢?實際上不管我用什么語言寫代碼,我的頭腦里都在用同一個語言構造程序。我寫代碼的過程,只不過是在為我腦子里的“萬能語言”找到對應的表達方式而已。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容