什么叫代碼的可讀性?為什么說Kotlin的可讀性比Java高?

不久之前,我看了一篇文章,大意是Kotlin與Java之間的對比,像這種文章,我一般是直接忽略的,但是那天我還是打開了,然后就看到一個非常吃驚的結果。
里面有一段是關于Kotlin與Java之間可讀性的對比的文章,作者的結論是:Kotlin并不比Java更具有可讀性,所有認為Kotlin 比Java更具有可讀性的結論都是“主觀性”的。
并且作者舉了一個在我看來,不知道該怎么來描述的例子:


image

這個作者的大意是,上面這段文章,你多讀個兩三遍,你也會很快的理解它的意思,所以“對于熟練的讀者而言,外觀很少會成為可讀性的障礙。”
我不知道,如果某一天,這個作者突發奇想,決定全部使用大寫字母來寫代碼——所有的類名、方法名、局部變量成員變量名等等全部使用大寫,我不知道跟作者合作的同事是不是會欣然的耐心的把作者所有的代碼先讀它個兩三遍,然后再來慢慢的理解它的意思。如果是我,我不會。如果在小紅書有個同事非要執意這樣寫代碼,理由是“你多讀個兩三遍不就好了嘛?”我想我只能把他開除了。
其實,如果一段代碼需要你多讀個兩三遍才能很好的理解,這本身不就說明,這段代碼的可讀性不高嗎?這里的重點是,這里的這一段大寫的文字你看個三遍,再看的話,是熟悉了,但是再看別的用大寫寫的文字片段,你依然要很費勁。所以,這個例子是不能代表大寫這種風格的可讀性的。在比較兩種不同的風格的可讀性的時候,你不能用具體的某一個一次性的片段來說明。
另外,這篇文章還暗含了這樣一個觀點,那就是,代碼的可讀性,僅僅是指,看到一段代碼,能不能理解這段代碼的含義。這是一個很多人都會錯誤的觀點。
但是,在真正工作中,代碼的可讀性,恐怕不至這一個方面。為了考察所謂代碼的可讀性涉及到哪些方面,我們來假設兩個case:1. 你去到一家新公司,接手一個新項目。這個時候,你的需求是,快速了解某個類、某個模塊、某個方法做的是什么事情。在這個基礎上,整個app、模塊的結構是怎么樣的。2. 你老板叫你fix一個bug,這個bug是另外一個同事寫的,今天這個同事請假了不在。在這個case里面,你需要的是,快速的定位到出問題的代碼在什么地方,然后再盡快的了解這個地方的代碼做了什么事情,并且保證你的理解是對的。
所以,總結一下,代碼的可讀性,可以歸納成三點:

  1. 理解一段代碼的速度
  2. 找到你關心的代碼的速度
  3. 確保正確理解一段代碼的難易程序。這跟第一點看似一樣,其實還真不一樣,下面你會看到。

下面,依次解釋一下這三點,以及為什么說,Kotlin的可讀性會對Java高。

1. 理解一段代碼的速度

如果大家仔細的思考下,你會發現,我們在理解一段代碼的時候,大多數情況下,我們是想要了解這段代碼做了什么事情,是這段代碼的意圖(Intention),而不是具體這個事情是怎么做的。比如一個Button被點擊了,我們的App做了什么,是做了什么運算,發了網絡請求,還是保證了一些數據到數據庫。也就是說,大多數情況下,我們關心的是What,而不是How。只有少數情況下,我們會關心“How”,一是出于學習的目的,我們想要了解一個算法是怎么實現的,一個效果是怎么實現的,這個時候,我們會關心“How”。二是當這個“How”出了問題的時候,就是有了Bug,我們要去了解這個 “How”,然后再fix過來。而且,即使是在這些少數情況下,了解“How”的過程,也只不過是了解一個個子“What”的過程。
敏捷開發和TDD先驅、JUnit開發作者和一系列經典編程書籍作者Kent Beck提出了一個著名的“four rules of simple design”,是以下4條:

  1. Passes the tests
  2. Reveals intention
  3. No duplication
  4. Fewest elements

第一條Passes the test說的是程序的正確性。第二條Reveals Intention,說的就是我們這里討論的“What”。

那么,Kotlin相對于Java,在幫助我們了解“What”,在幫助Reveals Intention這方面,有什么樣的優勢呢?
我們看一個簡單的例子:

image

在這段Java代碼例子中,這7行代碼做的事情很簡單,就是從personList中找出id值等于somePersonId這個變量的值的那個Person,然后賦值給person這個變量。要理解這段代碼并不難(其實后面你會看到,要確保正確理解這么代碼也沒那么簡單),但是速度并不快,你必須從頭到尾看完這8行代碼,就算你說最后兩行可以一掃而過,那也必須看完前面6行,你才能知道“哦,原來這段代碼做的事情是,從personList中找出id值等于。。。”

下面,我們來看對應的Kotlin代碼是怎么樣的:

val person = personList.find { it.id == somePersonId }

是的,就一行代碼。看完這行代碼,你就知道了它做的是什么事情。因為在這行代碼中,find這個單詞就已經表達出了這里做的事情是“找出一些東西”,而大括號里面,就是找出它的條件。也就是說,Kotlin的寫法直接就幫我們表達出了“What”。如果平均來說,一個人理解一行Java代碼的速度跟理解一行Kotlin代碼的速度是一樣的(雖然在我看來,理解一行Kotlin代碼會更容易,因為Kotlin里面有很多幫助開發者減輕負擔的語法,接下來會提到這一點),那么在這個例子中,一個人理解Kotlin代碼的速度是理解對應的Java代碼的5~6陪。之所以說5~,是因為在Java里面,你還可以寫成foreach語法,如果寫成foreach語法的話,那么Java代碼是5行。但是以我的經歷,多數情況下大家還是會習慣性的寫成fori,因為這兩者差別并沒那么大,優勢也不是那么明顯。
在Kotlin里面,Collection類有一整套像find這樣,直接可以reveal intention的方法,簡單點的有filtercountreducemapanyall等等,高級點的有mapTozipassociateflatMapgroupBy等等。絕大多數情況下,所有需要手動for循環的地方,都有對應的更加能“reveal intention”的方法。
當然,如果只有一個collection,就說Kotlin在Reveal Intention這點上比Java更有優勢,那是不夠的。Kotlin有一系列的機制和便利,能幫助開發者更好的達到“Reveal intension”的目。比如null的處理,ifwhen表達式(不是語句),比如循環的處理,比如所有對象都有的let, apply, run等方法,比如data class以及它的copy方法等等等等。此外,通過Extension Function這個機制,Kotlin對Java中絕大多數的常用類都作了擴展。前面提到的各種Collection方法,也是使用這種方式來進行擴展的。此外,就算有一些類沒有你想要的擴展,你也可以非常輕松容易的自己寫一個擴展方法,來讓你的代碼更加“Reveal Intension”。
相比之下,跟Kotlin相比,Java代碼更像逼我們去通過了解“how”之后,來總結歸納出它的“what”。在描述一門語言的時候,有一個術語叫做抽象程度,也就是一門言語表達“What”、屏蔽“How”的能力。從這點來說,無疑Kotlin的抽象程度是比Java要高的,就像是C語言的抽象程度比匯編要高一樣。實際上,我還還真有個朋友拿Java比作匯編。他是寫Scala的,有一天他這么跟我說

我之前一年多時間都是寫Scala的,現在我看到Java代碼,就像在看匯編一樣。

基于Kotlin的抽象程度更接近Scala,我想寫一年多Kotlin之后,你也會有類似的感覺。
OK,第一點講到這里。接下來我們來看第二點。

找到你關心的代碼的速度

當談到Kotlin的優勢時,有一點我相信是公認的,那就是Kotlin比Java更簡潔。而簡潔帶來的好處之一,就是能夠讓人更快的找到他關心的代碼,而不用在一堆雜七雜八的沒用的代碼里面去翻找自己在乎的代碼。我們還是以一個例子來說明吧,以下兩段代碼。

image

image

如果說,要你找出點擊loginButton以后,代碼做了什么事情,那以上兩段代碼中,無疑第二段代碼能讓你更快的找到。
上面這個例子還大大的簡化了很多東西,實際開發過程中,代碼更加復雜,Kotlin的優勢也更明顯。

確保正確理解一段代碼的難易程序

這是很多人會忽略的事情。能否理解一段代碼,跟確保正確的理解這段代碼,其實中間還是有一些差別的。很多代碼看起來很簡單,但是要確保自己正確的理解,其實還是非常費勁的。還是看文章開頭這個例子:

image

這一段代碼要確保正確的理解,容易嗎?其實沒那么容易,如果你工作年限多一點,你一定碰到過這樣的代碼,在for循環里面,i是從1開始的,而不是從0開始的,或者是中間的終止條件是i < personList.size() - 1,而不是i < personList.size(),或者最后部分不是i++,而是i = i + 2,或者i--。很多人更是碰到過很多bug,就是因為沒有看清楚這里面i的起始值、終止條件,或者是步長導致的。我就曾經碰到過很多這樣的bug,也曾經因為沒有注意這些地方,而導致過很多bug,最后調了半天,發現原來是因為for里面是i=1,而不是i=0。那時候,就只能在心里默默的大叫一聲:FUCK!
因為有這些非典型代碼的存在,所以現在,每次看到這樣寫的for循環,我心里都會覺得如履薄冰,會特別小心翼翼的看得很仔細,確保i的初始值是什么,終止條件是什么,步長是什么。這在無形之中會給人增加特別大的心理負擔。然而因為是無形之中的,是潛意識里面的,所以一般人不會注意到。畢竟,大家都這么寫,而且寫了幾十年了,能有什么問題呢?其實,是有的。這也是為什么Java5增加了Foreach語法的原因,然而可惜的是,大部分人并不清楚具體為什么要使用foreach,而且還聲稱fori比foreach性能更高,這真是令人遺憾。
說回Kotlin,那為什么說Kotlin代碼能讓人更容易正確的理解呢?
讓我們再看一下上面的例子對應的Kotlin代碼:

val person = personList.find { it.id == somePersonId }

在這一行代碼中,你根本無需擔心i的初始值、終止條件、步長,因為這里面根本沒有這些東西。所以,一個很大的心理擔負消失了。你也不用擔心這里面有沒有break,或者你是否忘了寫break。
這就是Kotlin讓代碼更容易理解的地方。同樣的,像這種減輕看代碼的人心理負擔的機制Kotlin里面有很多,這里再介紹一個很重要的“小”特性:使用val把一個變量定義成不可變的“變量”。我之前一篇文章說過,Kotlin的nullsafety是我最喜歡的特性,如果說第二喜歡的特性是什么,那就是val關鍵字。在團隊里面,我也一遍一遍的跟同事強調,能用val的地方就不要用var。因為它帶來的心理上的relief,是巨大的。我們看以下LinearLayout里面的代碼。

image

如果你寫了個自定義Layout繼承自LinearLayout,結果它表現出來的樣子不符合你的預期,你可能會去看源碼。看到上面這段,最后你發現,原來是mBaselineAlignedChildIndex這個成員變量的值不對。那么,你怎么知道是哪里改變了這個變量的值,導致它被賦給了一個錯誤的值呢?你可能要在這個類里面找出所有會改變這個變量的地方,然后一個一個去check,哪里會走到,哪里不會走到。更糟糕的是,也許這個值在某個public方法里面被改變了,這樣的話,你還要去找出所有調用這個public方法的地方,然后去確定到底是誰,在哪里調用了這個方法,而這些調用的地方又是誰調用的,導致出錯了。這想想就是一件非常頭疼的事情。
但是,如果這個值是final的話,這些麻煩就都不存在了。它的值要么是在它定義的地方就確定了,要么是在構造方法里面確定的,你只需要檢查兩個地方就可以了,這對于代碼理解,是一件極大的減少工作量的事情。這,就是為什么Effective Java里面,建議把所有能用final修飾的地方都用final修飾的原因。很多人認為,使用final是為了避免多線程同步的問題。但是,誠實的說,算了吧,作為安卓開發的你,上一次碰到多線程同步的原因導致一個變量的值出錯,是什么時候的事了呢?final的真正優點,在于讓人在看代碼的時候,不用到處去找可能會改變這個值的地方,也消去“這個值會不會在哪里被改變”的大心理負擔。
思考深入的讀者可能會發現,其實上面的這個例子有點矛盾。因為我說的是使用final來定義變量,但是像上面的mBaselineAlignedChildIndex這個成員變量,是不能加final的,因為它就是要可變的啊,它就是需要在某些條件下被重新賦值的啊,這不是矛盾了嗎?
是的,很多時候,我們不能使用final來定義一個成員變量。但是,如果你試著給那些可以加上final的成員變量加上final,你會發現,其實大部分成員變量和幾乎所有局部變量都是可以加上final的,但是現實情況是什么呢?是幾乎所有的成員變量和局部變量,我們都沒有使用final來定義。我們寫代碼的默認設置是,先不加final,如果在哪個地方編譯出錯了----比如寫一個匿名內部類,引用了前面的局部變量----迫使我們使用final來修飾一個變量的時候,我們才加上。
為什么會出現這種情況呢?有兩點原因:

  1. final的好處并不為大家所知,也不是一眼能看出來的。
  2. 使用final要寫多寫一個單詞。

當一個東西的優勢不是很容易被識別(注意,不容易被識別,不代表這個優勢不大,或者不重要,這是兩回事),同時又要多付出一些努力的時候,我們寫代碼的默認設置是不加final,這就非常合情合理了。
那Kotlin在這點上,又有什么優勢呢?Kotlin的優勢有幾個,先講一個不起眼的優勢:使用val來定義“變量”。這里之所以給“變量”加上雙引號,是因為使用val來定義的“變量”一旦被賦值,是不能被改變的,所以好像稱他們為“變”量不大合適。但我又找不到合適的詞來叫這個東西,所以暫且還是稱作“變量”吧。
不要小看了這個優勢。當你可以使用varval這兩個看起來寫起來都差別不大的方式來定義一個東西的時候,人們自然會想要去了解,這兩者到底有什么區別?我應該使用哪個?這時候,就會有一些討論,有一些標準出來,人們就會認識到,不可變性(Immutability)原來有這么大的價值,原來是這么好的一個東西。同時,因為valvar寫起來沒有差別,所以人們也會更愿意使用val來定義“變量”。
當然,要我說,kotlin這一點做得還不夠。應該像Rust一樣,讓可變的變量定義起來比不可變的變量定義起來更費勁,這才能更加促進不可變量這種好的practice的發揚光大。

image

在StackOverflow的調查中(20172016),Rust連續幾年被評為“程序員最喜愛的語言(Most Loved)”,這不是沒有原因的,它的設定也不是沒有原因的。
除此之外,Kotlin還使用了一些方式,來讓原本不能定義為val的變量,也可以使用val來定義,比如by lazylateinit,這些機制綜合起來,即讓val寫起來很容易,也擴大了val的適合范圍。
上面花了很多篇幅來解釋,Kotlin中val的價值。跟Collection中的眾多擴展方法一樣,這些都是Kotlin中,一些讓代碼更容易理解的機制。像這樣的機制還有很多,比如說在Kotlin中,if、when(Kotlin中的switch)都是表達式(Expression,會返回一些值),而不像在Java中,只是語句(Statement,不會有返回值),比如說對null的處理,如果你看過多層嵌套的null判斷,你就知道那種代碼看起來有多費勁了。而使用Kotlin,結合val,在定義的時候把它定義成非null,你可以明顯的告訴代碼的讀者,也告訴你自己,這個地方是不需要進行null判斷的。這就大大的減少了null判斷的數量。
由于篇幅的關系,這些還有剩下的一些機制,這里就不展開講了。當你寫kotlin代碼的時候,多思考一下,Kotlin為什么要這樣設定,你就會明白,都是有原因的,多數情況下,都是有優勢的。

為什么代碼的可讀性這么重要?

以上從三個方面解釋了什么叫代碼的可讀性,可以看到,無論在哪個方面,Kotlin都有比Java更大的優勢。那接下來的一個問題就是,So what?可讀性有這么重要嗎?能吃嗎?值多少錢?
別說,可讀性還真可以吃,而且很值錢!
關于可讀性的重要性,其實上面分析什么叫可讀性的時候,已經提到了,這里歸納一下,只說兩點:

  1. 更快的找到你關心的代碼,更快的理解代碼。
    要知道,我們現實開發過程中,大部分時間是在看代碼,而不是在寫代碼。更快的理解代碼,意味著更高的工作效率,節省更多的時間,時間就是金錢,所以更高的可讀性,意味著省錢。或者用省下來的時間去賺更多的錢。
  2. 更容易正確的理解代碼,從而不會因為對老代碼的理解不到位而改錯,造成新的bug。
    大家可以回想一下,過去有多少bug的發生,是因為對遺留代碼的理解不到位,不全面導致的呢?在小紅書,這個比例不少,也造成過不小的問題。痛定思痛,我們現在能做的,就是引以為戒。寫代碼的時候,重視可讀性,讓后來的人,讓后來的自己,不要再吃這樣的虧,不要再背這樣的鍋。

“人生苦短,快用Kotlin,珍愛生命,遠離Java”。這是小紅書安卓客戶端Coding Style的第一條規則。順便說一句,使用Kotlin之后,目前我們的app crash原因里面,Top10沒有任何一個NullPointerException了,Top20有兩個,這兩個都是發生在系統層面的,也就是說,我們自己代碼里面的NullPointerException,基本都已經消滅了。而在6月份,上Kotlin之前,我有統計過的一次,Crash里面Top10有5個NullPointerException,Top20里面有12個。所以Kotlin的作用,是非常明顯的。目前,小紅書總體的Crash率,除非意外發生,可以很輕松的保持在0.2%甚至0.1%之內,這在業界,即便不是最好的水平,也是個非常好的水平。

在Google聲明Kotlin成為安卓開發的官方支持語言那一天,我建了一個微信群。目前偶爾還是有人在群里面問,Kotlin值得學習嗎?有風險嗎?看到現在還在問這樣的問題,我是覺得有點遺憾的。希望上面的文章能讓你從代碼可讀性的角度,了解Kotlin的優勢。讓好的技術在這個世界盛行,對技術人來說,是一件非常值得欣慰的事情。

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

推薦閱讀更多精彩內容