天天看點

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

點選檢視第一章 點選檢視第三章

第2章

基礎文法

在明白Kotlin的設計哲學之後,你可能迫不及待地想要了解它的具體語言特性了。本章我們會介紹Kotlin中最基礎的文法和特點,包括:

  • 程式中最基本的操作,如聲明變量、定義函數以及字元串操作;
  • 高階函數的概念,以及函數作為參數和傳回值的作用;
  • Lambda表達式文法,以及用它來簡化程式表達;
  • 表達式在Kotlin中的特殊設計,以及if、when、try等表達式的用法。

由于這是一門旨在成為更好的Java而被設計出來的語言,我們會在介紹它的某些特性的同時,與Java中相似的文法進行對比,這樣可以讓你更好地認識Kotlin。好了,我們現在就開始吧。

2.1 不一樣的類型聲明

當你學習Kotlin時,可能第一個感到與衆不同的文法就是聲明變量了。在Java中,我們會把類型名放在變量名的前面,如此來聲明一個變量。

String a = "I am Java";

Kotlin采用的則是不同的做法,與Java相反,類型名通常在變量名的後面。

val a: String = "I am Kotlin"

為什麼要采用這種風格呢?以下是Kotlin官方FAQ的回答:

我們相信這樣可以使得代碼的可讀性更好。同時,這也有利于使用一些良好的文法特性,比如省略類型聲明。Scala的經驗表明,這不是一個錯誤的選擇。

很好,我們發現Kotlin确實在簡潔、優雅的文法表達這一目标上表現得言行一緻。同時你也可能注意到了關于“省略類型聲明”的描述,這是什麼意思呢?

2.1.1 增強的類型推導

類型推導是Kotlin在Java基礎上增強的語言特性之一。通俗地了解,編譯器可以在不顯式聲明類型的情況下,自動推導出它所需要的類型。我們來寫幾個例子:

val string = "I am Kotlin"

val int = 1314

val long = 1314L

val float = 13.14f

val double = 13.34

val double2 = 10.1e6

然後在REPL中列印以上變量的類型,如println(string.javaClass.name),獲得的結果如下:

java.lang.String

int

long

float

double

類型推導在很大程度上提高了Kotlin這種靜态類型語言的開發效率。雖然靜态類型的語言有很多的優點,然而在編碼過程中卻需要書寫大量的類型。類型推導則可幫助Kotlin改善這一情況。當我們用Kotlin編寫代碼時,IDE還會基于類型推導提供更多的提醒資訊。

在本書接下來展示的Kotlin代碼中,你會經常感受到類型推導的魅力。

2.1.2 聲明函數傳回值類型

雖然Kotlin在很大程度上支援了類型推導,但這并不意味着我們就可以不聲明函數傳回值類型了。先來看看Kotlin如何用fun關鍵字定義一個函數:

fun sum(x: Int, y: Int): Int { return x + y }

與聲明變量一樣,類型資訊放在函數名的後面。現在我們把傳回類型聲明去掉試試:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

在以上的例子中,因為沒有聲明傳回值的類型,函數會預設被當成傳回Unit類型,然而實際上傳回的是Int,是以編譯就會報錯。這種情況下我們必須顯式聲明傳回值類型。

由于一些語言如Java沒有Unit類型,你可能不是很熟悉。不要緊,目前你可以暫時把它當作類似Java中的void。不過它們顯然是不同的,Unit是一個類型,而void隻是一個關鍵字,我們會在2.4.2節進一步比較兩者。

也許你會說,Kotlin看起來并沒有比Java強多少嘛,Java也支援某種程度上的類型推導,比如Java 7開始已經支援泛型上的類型推導,Java 10則進一步支援了“局部變量”的類型推導。

其實,Kotlin進一步增強了函數的文法,我們可以把{}去掉,用等号來定義一個函數。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

Kotlin支援這種用單行表達式與等号的文法來定義函數,叫作表達式函數體,作為區分,普通的函數聲明則可叫作代碼塊函數體。如你所見,在使用表達式函數體的情況下我們可以不聲明傳回值類型,這進一步簡化了文法。但别高興得太早,再來一段遞歸程式試試看:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

你可能覺察到了if在這裡不同尋常的用法—沒有return關鍵字。在Kotlin中,if是一個表達式,它的傳回值類型是各個邏輯分支的相同類型或公共父類型。

表達式在Kotlin中占據了非常重要的地位,我們會在2.4節重點介紹這一特性。

我們發現,目前編譯器并不能針對遞歸函數的情況推導類型。由于像Kotlin、Scala這類語言支援子類型和繼承,這導緻類型系統很難做到所謂的全局類型推導。

關于全局類型推導(global type inference),純函數語言Haskell是一個典型的代表,它可以在以上的情況下依舊推導出類型。

是以,在一些諸如遞歸的複雜情況下,即使用表達式定義函數,我們也必須顯式聲明類型,才能讓程式正常工作。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,如果這是一個表達式定義的接口方法,顯式聲明類型雖然不是必需的,但可以在很大程度上提升代碼的可讀性。

總結

我們可以根據以下問題的提示,來判斷是否需要顯式聲明類型:

  • 如果它是一個函數的參數?

  必須使用。

  • 如果它是一個非表達式定義的函數?

  除了傳回Unit,其他情況必須使用。

  • 如果它是一個遞歸的函數?
  • 如果它是一個公有方法的傳回值?

為了更好的代碼可讀性及輸出類型的可控性,建議使用。

除上述情況之外,你可以盡量嘗試不顯式聲明類型,直到你遇到下一個特殊情況。

2.2 val和var的使用規則

與Java另一點不同在于,Kotlin聲明變量時,引入了val和var的概念。var很容易了解,JavaScript等其他語言也通過該關鍵字來聲明變量,它對應的就是Java中的變量。那麼val又代表什麼呢?

如果說var代表了varible(變量),那麼val可看成value(值)的縮寫。但也有人覺得這樣并不直覺或準确,而是把val解釋成varible+final,即通過val聲明的變量具有Java中的final關鍵字的效果,也就是引用不可變。

我們可以在IntelliJ IDEA或Android Studio中檢視val文法反編譯後轉化的Java 代碼,從中可以很清楚地發現它是用final實作這一特性的。

2.2.1 val的含義:引用不可變

val的含義雖然簡單,但依然會有人迷惑。部分原因在于,不同語言跟val相關的語言特性存在差異,進而容易導緻誤解。

我們先用val聲明一個指向數組的變量,然後嘗試對其進行修改。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

因為引用不可變,是以x不能指向另一個數組,但我們可以修改x指向數組的值。

如果你熟悉Swift,自然還會聯想到let,于是我們再把上面的代碼翻譯成Swift的版本。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這下連引用數組的值都不能修改了,這是為什麼呢?

其實根本原因在于兩種語言對數組采取了不同的設計。在Swift中,數組可以看成一個值類型,它與變量x的引用一樣,存放在棧記憶體上,是不可變的。而Kotlin這種語言的設計思路,更多考慮數組這種大資料結構的拷貝成本,是以存儲在堆記憶體中。

是以,val聲明的變量是隻讀變量,它的引用不可更改,但并不代表其引用對象也不可變。事實上,我們依然可以修改引用對象的可變成員。如果把數組換成一個Book類的對象,如下編寫方式會變得更加直覺:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

首先,這裡展示了Kotlin中的類不同于Java的構造方法,我們會在第3章中介紹關于它具體的文法。其次,我們發現var和val還可以用來聲明一個類的屬性,這也是Kotlin中一種非常有個性且有用的文法,你還會在後續的資料類中再次接觸到它的應用。

2.2.2 優先使用val來避免副作用

在很多Kotlin的學習資料中,都會傳遞一個原則:優先使用val來聲明變量。這相當正确,但更好的了解可以是:盡可能采用val、不可變對象及純函數來設計程式。關于純函數的概念,其實就是沒有副作用的函數,具備引用透明性,我們會在第10章專門探讨這些概念。由于後續的内容我們會經常使用副作用來描述程式的設計,是以我們先大概了解一下什麼是副作用。

簡單來說,副作用就是修改了某處的某些東西,比方說:

  • 修改了外部變量的值。
  • IO操作,如寫資料到磁盤。
  • UI操作,如修改了一個按鈕的可操作狀态。

來看個實際的例子:我們先用var來聲明一個變量a,然後在count函數内部對其進行自增操作。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章
帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

在以上代碼中,我們會發現多次調用count(1)得到的結果并不相同,顯然這是受到了外部變量 a 的影響,這個就是典型的副作用。如果我們把var換成val,然後再執行類似的操作,編譯就會報錯。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這就有效避免了之前的情況。當然,這并不意味着用val聲明變量後就不能再對該變量進行指派,事實上,Kotlin也支援我們在一開始不定義val變量的取值,随後再進行指派。然而,因為引用不可變,val聲明的變量隻能被指派一次,且在聲明時不能省略變量類型,如下所示:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

不難發現副作用的産生往往與可變資料及共享狀态有關,有時候它會使得結果變得難以預測。比如,我們在采用多線程處理高并發的場景,“并發通路”就是一個明顯的例子。然而,在Kotlin程式設計中,我們推薦優先使用val來聲明一個本身不可變的變量,這在大部分情況下更具有優勢:

  • 這是一種防禦性的編碼思維模式,更加安全和可靠,因為變量的值永遠不會在其他地方被修改(一些架構采用反射技術的情況除外);
  • 不可變的變量意味着更加容易推理,越是複雜的業務邏輯,它的優勢就越大。

回到在Java中進行多線程開發的例子,由于Java的變量預設都是可變的,狀态共享使得開發工作很容易出錯,不可變性則可以在很大程度上避免這一點。當然,我們說過,val隻能確定變量引用的不可變,那如何保證引用對象的不可變性?你會在第6章關于隻讀集合的介紹中發現一種思路。

2.2.3 var的适用場景

一個可能被提及的問題是:既然val這麼好,那麼為什麼Kotlin還要保留var呢?

事實上,從Kotlin誕生的那一刻就決定了必須擁抱var,因為它相容Java。除此之外,在某些場景使用var确實會起到不錯的效果。舉個例子,假設我們現在有一個整數清單,然後周遊元素操作後獲得計算結果,如下:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這是我們非常熟悉的做法,以上代碼中的res是個局部的可變變量,它與外界沒有任何互動,非常安全可控。我們再來嘗試用val實作:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這就有點尴尬了,必須利用遞歸才能實作,原本非常簡單的邏輯現在變得非常不直覺。當然,熟悉Kotlin的朋友可能知道List有一個fold方法,可以實作一個更加精簡的版本。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

函數式API果然擁有極強的表達能力。

可見,在諸如以上的場合下,用var聲明一個局部變量可以讓程式的表達顯得直接、易于了解。這種例子很多,即使是Kotlin的源碼實作,尤其集合類周遊的實作方法,也大量使用了var。之是以采用這種指令式風格,而不是更簡潔的函數式實作,一個很大的原因是因為var的方案有更好的性能,占用記憶體更少。是以,尤其針對資料結構,可能在業務中需要存儲大量的資料,是以顯然采用var是其更加适合的實作方案。

2.3 高階函數和Lambda

通過2.1節的介紹,我們發現Kotlin中的函數定義要更加簡潔、靈活。這一節我們會介紹關于函數更加進階的特性—高階函數和Lambda。由于在後續的内容中你要經常跟它們打交道,是以在開始不久我們就要充分地了解它們。

我們說過,Kotlin天然支援了部分函數式特性。函數式語言一個典型的特征就在于函數是頭等公民—我們不僅可以像類一樣在頂層直接定義一個函數,也可以在一個函數内部定義一個局部函數,如下所示:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,我們還可以直接将函數像普通變量一樣傳遞給另一個函數,或在其他函數内被傳回。如何了解這個特性呢?

2.3.1 抽象和高階函數

《計算機程式的構造和解釋》這本經典的書籍的開篇,有一段關于抽象這個概念的描述:

心智的活動,除了盡力産生各種簡單的認識外,主要表現在如下3個方面:

1)将若幹簡單的認識組合為一個複合認識,由此産生出各種複雜的認識;

2)将兩個認識放在一起對照,不管它們如何簡單或者複雜,在這樣做時并不将它們合二為一;由此得到有關它們的互相關系的認識;

3)将有關認識與那些在實際中和它們同在的所有其他認識隔離開,這就是抽象,所有具有普遍性的認識都是這樣得到的。

簡單地了解,我們會善于對熟悉或重複的事物進行抽象,比如2歲左右的小孩就會開始認知數字1、2、3……之後,我們總結出了一些公共的行為,如對數字做加減、求立方,這被稱為過程,它接收的數字是一種資料,然後也可能産生另一種資料。過程也是一種抽象,幾乎我們所熟悉的所有進階語言都包含了定義過程的能力,也就是函數。

然而,在我們以往熟悉的程式設計中,過程限制為隻能接收資料為參數,這個無疑限制了進一步抽象的能力。由于我們經常會遇到一些同樣的程式設計模式能夠用于不同的過程,比如一個包含了正整數的清單,需要對它的元素進行各種轉換操作,例如對所有元素都乘以3,或者都除以2。我們就需要提供一種模式,同時接收這個清單及不同的元素操作過程,最終傳回一個新的清單。

為了把這種類似的模式描述為相應的概念,我們就需要構造出一種更加進階的過程,表現為:接收一個或多個過程為參數;或者以一個過程作為傳回結果。

這個就是所謂的高階函數,你可以把它了解成“以其他函數作為參數或傳回值的函數”。高階函數是一種更加進階的抽象機制,它極大地增強了語言的表達能力。

2.3.2 執行個體:函數作為參數的需求

以上關于高階函數的闡述可能讓你對它建立了初步的印象,然而依舊不夠清晰。接下來,我們具體看下函數作為參數到底有什麼用。需要注意的是,《Java 8實戰》通過一個實作filter方法的例子,很好地展現了函數參數化的作用,我們會采用類似的思路,用實際例子來探讨函數作為參數的需求,以及Kotlin相關的文法特性。

Shaw因為旅遊喜歡上了地理,然後他建了一個所有國家的資料庫。作為一名程式員,他設計了一個CountryApp類對國家資料進行操作。Shaw偏好歐洲的國家,于是他設計了一個程式來擷取歐洲的所有國家。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

以上我們用data class聲明了一個Country資料類,目前也許你會感覺陌生,我們會在下一章詳細介紹這種文法。

後來,Shaw對非洲也産生了興趣,于是他又改進了上述方法的實作,支援根據具體的洲來篩選國家。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

以上的程式具備了一定的複用性。然而,Shaw的地理知識越來越豐富了,他想對國家的特點做進一步的研究,比如篩選具有一定人口規模的國家,于是代碼又變成下面這個樣子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章
帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

新增了一個population的參數來代表人口(機關:萬)。Shaw開始感覺到不對勁,如果按照現有的設計,更多的篩選條件會作為方法參數而不斷累加,而且業務邏輯也高度耦合。

解決問題的核心在于對filterCountries方法進行解耦,我們能否把所有的篩選邏輯行為都抽象成一個參數呢?傳入一個類對象是一種解決方法,我們可以根據不同的篩選需求建立不同的子類,它們都各自實作了一個校驗方法。然而,Shaw了解到Kotlin是支援高階函數的,理論上我們同樣可以把篩選的邏輯變成一個方法來傳入,這種思路更加簡單。

他想要進一步了解這種進階的特性,是以很快就寫了一個新的測試類,如代碼清單2-1所示。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

調用isBigEuropeanCountry方法就能夠判斷一個國家是否是一個人口超過1億的歐洲國家。然而,怎樣才能把這個方法變成filterCountries方法的一個參數呢?要實作這一點似乎要先解決以下兩個問題:

  • 方法作為參數傳入,必須像其他參數一樣具備具體的類型資訊。
  • 需要把isBigEuropeanCountry的方法引用當作參數傳遞給filterCountries。

接下來,我們先來研究第1個問題,即Kotlin中的函數類型是怎樣的。

2.3.3 函數的類型

在Kotlin中,函數類型的格式非常簡單,舉個例子:

(Int) -> Unit

從中我們發現,Kotlin中的函數類型聲明需遵循以下幾點:

  • 通過->符号來組織參數類型和傳回值類型,左邊是參數類型,右邊是傳回值類型;
  • 必須用一個括号來包裹參數類型;
  • 傳回值類型即使是Unit,也必須顯式聲明。

如果是一個沒有參數的函數類型,參數類型部分就用()來表示。

() -> Unit

如果是多個參數的情況,那麼我們就需要用逗号來進行分隔,如:

(Int, String) -> Unit

此外,Kotlin還支援為聲明參數指定名字,如下所示:

(errCode: Int, errMsg: String) -> Unit

在本書的第5章中我們還會介紹Kotlin中的可空類型,它将支援用一個“?”來表示類似Java 8中Optional類的效果。如果errMsg在某種情況下可空,那麼就可以如此聲明類型:

(errCode: Int, errMsg: String?) -> Unit

如果該函數類型的變量也是可選的話,我們還可以把整個函數類型變成可選:

((errCode: Int, errMsg: String?) -> Unit)?

這種組合是不是非常有意思?還沒完,我們說過,高階函數還支援傳回另一個函數,是以還可以這麼做:

(Int) -> ((Int) -> Unit)

這表示傳入一個類型為Int的參數,然後傳回另一個類型為(Int) -> Unit的函數。簡化它的表達,我們可以把後半部分的括号給省略:

(Int) -> Int -> Unit

需要注意的是,以下的函數類型則不同,它表示的是傳入一個函數類型的參數,再傳回一個Unit。

((Int) -> Int) -> Unit

好了,在學習了Kotlin函數類型知識之後,Shaw便重新定義了filterCountries方法的參數聲明。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

那麼,下一個問題來了。我們如何才能把代碼清單2-1中的isBigEuropeanCountry方法傳遞給filterCountries呢?直接把isBigEuropeanCountry當參數肯定不行,因為函數名并不是一個表達式,不具有類型資訊,它在帶上括号、被執行後才存在值。可以看出,我們需要的是一個單純的方法引用表達式,用它在filterCountries内部來調用參數。下一節我們會具體介紹如何使用這種文法。

2.3.4 方法和成員引用

Kotlin存在一種特殊的文法,通過兩個冒号來實作對于某個類的方法進行引用。以上面的代碼為例,假如我們有一個CountryTest類的對象執行個體countryTest,如果要引用它的isBigEuropeanCountry方法,就可以這麼寫:

countryTest::isBigEuropeanCountry

為什麼使用雙冒号的文法?

如果你了解C#,會知道它也有類似的方法引用特性,隻是文法上不同,是通過點号來實作的。然而,C#的這種方式存在二義性,容易讓人混淆方法引用表達式與成員表達式,是以Kotlin采用::(沿襲了Java 8的習慣),能夠讓我們更加清晰地認識這種文法。

此外,我們還可以直接通過這種文法,來定義一個類的構造方法引用變量。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

可以發現,getBook的類型為(name: String) -> Book。類似的道理,如果我們要引用某個類中的成員變量,如Book類中的name,就可以這樣引用:

Book::name

以上建立的Book::name的類型為(Book) -> String。當我們在對Book類對象的集合應用一些函數式API的時候,這會顯得格外有用,比如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

我們會在6.2節再次提到這種應用。

于是,Shaw便使用了方法引用來傳遞參數,以下的調用果真奏效了。

val countryApp = CountryApp()

val countryTest = CountryTest()

val countries = ……

countryApp.filterCountries(countries, countryTest::isBigEuropeanCountry)

經過重構後的程式顯然比之前要優雅許多,程式可以根據任意的篩選需求,調用同一個filterCountries方法來擷取國家資料。

2.3.5 匿名函數

再來思考下代碼清單2-1的CountryTest類,這仍算不上一種很好的方案。因為每增加一個需求,我們都需要在類中專門寫一個新增的篩選方法。然而Shaw的需求很多都是臨時性的,不需要被複用。Shaw覺得這樣還是比較麻煩,他打算用匿名函數對程式做進一步的優化。

Kotlin支援在預設函數名的情況下,直接定義一個函數。是以isBigEuropeanCountry方法我們可以直接定義為:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

于是,Shaw直接調用filterCountries,如代碼清單2-2所示。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這一次我們甚至都不需要CountryTest這個類了,代碼的簡潔性又上了一層樓。Shaw開始意識到Kotlin這門語言的魅力,很快他發現還有一種文法可以讓代碼更簡單,這就是Lambda表達式。

2.3.6 Lambda是文法糖

提到Lambda表達式,也許你聽說過所謂的Lambda演算。其實這是兩個不同的概念,Lambda演算和圖靈機一樣,是一種支援理論上完備的形式系統,也是了解函數式程式設計的理論基礎。古老的Lisp語言就是基于Lambda演算系統而來的,在Lisp中,匿名函數是重要的組成部分,它也被叫作Lambda表達式,這就是Lambda表達式名字的由來。是以,相較Lambda演算而言,Lambda表達式是更加簡單的概念。你可以把它了解成簡化表達後的匿名函數,實質上它就是一種文法糖。

我們先來分析下代碼清單2-2中的filterCountries方法的匿名函數,會發現:

  • fun(country:Country)顯得比較啰唆,因為編譯器會推導類型,是以隻需要一個代表變量的country就行了;
  • return關鍵字也可以省略,這裡傳回的是一個有值的表達式;
  • 模仿函數類型的文法,我們可以用->把參數和傳回值連接配接在一起。

是以,簡化後的表達就變成了這個樣子:

countryApp.filterCountries(countries, {

country ->
country.continient == "EU" && country.population > 10000           

})

是不是非常簡潔?這個就是Lambda表達式,它與匿名函數一樣,是一種函數字面量。我們再來講解下Lambda具體的文法。現在用Lambda的形式來定義一個加法操作:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

由于支援類型推導,我們可以采用兩種方式進行簡化:

val sum = { x: Int, y: Int -> x + y }

或者是:

val sum: (Int, Int) -> Int = { x, y -> x + y }

現在來總結下Lambda的文法:

  • 一個Lambda表達式必須通過{}來包裹;
  • 如果Lambda聲明了參數部分的類型,且傳回值類型支援類型推導,那麼Lambda變量就可以省略函數類型聲明;
  • 如果Lambda變量聲明了函數類型,那麼Lambda的參數部分的類型就可以省略。

此外,如果Lambda表達式傳回的不是Unit,那麼預設最後一行表達式的值類型就是傳回值類型,如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

Lambda看起來似乎很簡單。那麼再思考一個場景,如果用fun關鍵字來聲明Lambda表達式又會怎麼樣?如代碼清單2-3所示。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

1.單個參數的隐式名稱

首先,也許你在it這個關鍵字上停留了好幾秒,然後依舊不明其意。其實它也是Kotlin簡化Lambda表達的一種文法糖,叫作單個參數的隐式名稱,代表了這個Lambda所接收的單個參數。這裡的調用等價于:

listOf(1, 2, 3).forEach { item -> foo(item) }

預設情況下,我們可以直接用it來代表item,而不需要用item->進行聲明。

其次,這行代碼的結果可能出乎了你的意料,執行後你會發現什麼也沒有。為什麼會這樣?這一次,我們必須要借助IDE的幫助了,以下是把foo函數用IDE轉化後的Java代碼:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

以上是位元組碼反編譯的Java代碼,從中我們可以發現Kotlin實作Lambda表達式的機理。

2. Function類型

Kotlin在JVM層設計了Function類型(Function0、Function1……Function22)來相容Java的Lambda表達式,其中的字尾數字代表了Lambda參數的數量,如以上的foo函數建構的其實是一個無參Lambda,是以對應的接口是Function0,如果有一個參數那麼對應的就是Function1。它在源碼中是如下定義的:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

可見每個Function類型都有一個invoke方法,稍後會詳細介紹這個方法。

設計Function類型的主要目的之一就是要相容Java,實作在Kotlin中也能調用Java的Lambda。在Java中,實際上并不支援把函數作為參數,而是通過函數式接口來實作這一特性。是以如果我們要把Java的Lambda傳給Kotlin,那麼它們就必須實作Kotlin的Function接口,在Kotlin中我們則不需要跟它們打交道。在第6章我們會介紹如何在Kotlin中調用Java的函數式接口。

神奇的數字—22

也許你會問一個問題:為什麼這裡Function類型最大的是Function22?如果Lambda的參數超過了22個,那該怎麼辦呢?

雖然22個參數已經夠多了,然而現實中也許我們真的需要超過22個參數。其實,在Scala的設計中也遵循了22個數字的設計,這似乎已經成了業界的一種慣例。然而,這個22的設計也給Scala開發者帶來了不少麻煩。是以,Kotlin在設計的時候便考慮到了這種情況,除了23個常用的Function類型外,還有一個FunctionN。在參數真的超過22個的時候,我們就可以依靠它來解決問題。更多細節可以參考

https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md

3. invoke方法

代碼清單2-3中的foo函數的傳回類型是Function0。這也意味着,如果我們調用了foo(n),那麼實質上僅僅是構造了一個Function0對象。這個對象并不等價于我們要調用的過程本身。通過源碼可以發現,需要調用Function0的invoke方法才能執行println方法。是以,我們的疑惑也迎刃而解,上述的例子必須如下修改,才能夠最終列印出我們想要的結果:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

也許你覺得invoke這種文法顯得醜陋,不符合Kotlin簡潔表達的設計理念。确實如此,是以我們還可以用熟悉的括号調用來替代invoke,如下所示:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

2.3.7 函數、Lambda和閉包

在你不熟悉Kotlin文法的情況下,很容易對fun聲明函數、Lambda表達式的文法産生混淆,因為它們都可以存在花括号。現在我們已經了解了它們具體的文法,可通過以下的總結來更好地區分:

  • fun在沒有等号、隻有花括号的情況下,是我們最常見的代碼塊函數體,如果傳回非Unit值,必須帶return。
  1. foo(x: Int) { print(x) }

fun foo(x: Int, y: Int): Int { return x

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

y }

  • fun帶有等号,是單表達式函數體。該情況下可以省略return。
  1. foo(x: Int, y: Int) = x + y

不管是用val還是fun,如果是等号加花括号的文法,那麼建構的就是一個Lambda表達式,Lambda的參數在花括号内部聲明。是以,如果左側是fun,那麼就是Lambda表達式函數體,也必須通過()或invoke來調用Lambda,如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

在Kotlin中,你會發現匿名函數體、Lambda(以及局部函數、object表達式)在文法上都存在“{}”,由這對花括号包裹的代碼塊如果通路了外部環境變量則被稱為一個閉包。一個閉包可以被當作參數傳遞或者直接使用,它可以簡單地看成“通路外部環境變量的函數”。Lambda是Kotlin中最常見的閉包形式。

與Java不一樣的地方在于,Kotlin中的閉包不僅可以通路外部變量,還能夠對其進行修改,就像這樣子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,Kotlin還支援一種自運作的Lambda文法:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

執行以上代碼,結果會列印1。

2.3.8 “柯裡化”風格、擴充函數

我們已經知道,函數參數化是一種十分強大的特性,結合Lambda表達式,能在很大程度上提高語言的抽象和表達能力。接下來,我們再來了解下高階函數在Kotlin中另一方面的表現,即一個函數傳回另一個函數作為結果。

通過之前的介紹,相信你已經能很容易地了解什麼是傳回一個函數。還記得我們上面使用過的例子嗎?

fun foo(x: Int) = { y: Int -> x + y }

表達上非常簡潔,其實它也可以等價于:

fun foo(x: Int): (Int) -> Int {

return { y: Int -> x + y }           

}

現在有了函數類型資訊之後,可以很清晰地發現,執行foo函數之後,會傳回另一個類型為(Int) -> Int的函數。

如果你看過一些介紹函數式程式設計的文章,可能聽說過一種叫作“柯裡化(Currying)”的文法,其實它就是函數作為傳回值的一種典型的應用。

簡單來說,柯裡化指的是把接收多個參數的函數變換成一系列僅接收單一參數函數的過程,在傳回最終結果值之前,前面的函數依次接收單個參數,然後傳回下一個新的函數。

拿我們最熟悉的加法舉例子,以下是多參數的版本:

fun sum(x: Int, y: Int, z: Int) = x + y + z

sum(1, 2, 3)

如果我們把它按照柯裡化的思路重新設計,那麼最終就可以實作鍊式調用:

fun sum(x: Int) = { y: Int ->

{ z: Int -> x + y + z }           

sum(1)(2)(3)

你會發現,柯裡化非常類似“擊鼓傳花”的遊戲,遊戲開始時有個暗号,第1個人将暗号進行演繹,緊接着第2個人演繹,依次類推,經過一系列加工之後,最後一個人揭曉謎底。在這個過程中:

  • 開始的暗号就是第1個參數;
  • 下個環節的演繹就是傳回的函數;
  • 謎底就是柯裡化後最終執行獲得的結果。

可見柯裡化是一個比較容易了解的概念,那麼為什麼會有柯裡化呢?

柯裡化與Lambda演算

我們說過,Lambda演算是函數式語言的理論基礎。在嚴格遵守這套理論的設計中,所有的函數都隻能接收最多一個參數。為了解決多參數的問題,Haskell Curry引入了柯裡化這種方法。值得一提的是,這種技術也是根據他的名字來命名的—Currying,後續其他語言也以此來稱呼它。

說到底,柯裡化是為了簡化Lambda演算理論中函數接收多參數而出現的,它簡化了理論,将多元函數變成了一進制。然而,在實際工程中,Kotlin等語言并不存在這種問題,因為它們的函數都可以接收多個參數進行計算。那麼,這是否意味着柯裡化對我們而言,僅僅隻有理論上的研究價值呢?雖然柯裡化在工程中并沒有大規模的應用,然而在某些情況下确實起到了某種奇效。

在我們之前介紹過的Lambda表達式中,還存在一種特殊的文法。如果一個函數隻有一個參數,且該參數為函數類型,那麼在調用該函數時,外面的括号就可以省略,就像這樣子:

fun omitParentheses(block: () -> Unit) {

block()           

omitParentheses {

println("parentheses is omitted")           

此外,如果參數不止一個,且最後一個參數為函數類型時,就可以采用類似柯裡化風格的調用:

fun curryingLike(content: String, block: (String) -> Unit) {

block(content)           

curryingLike("looks like currying style") {

content ->
println(content)           

// 運作結果

looks like currying style

它等價于以下的的調用方式:

curryingLike("looks like currying style", {

content ->
println(content)           

實際上,在Scala中我們就是通過柯裡化的技術,實作以上簡化文法的表達效果的。Kotlin則直接在語言層面提供了這種文法糖,這個确實非常友善。然而需要注意的是,通過柯裡化實作的方案,我們還可以分步調用參數,傳回值是一個新的函數。

curryingLike("looks like currying style")

// 運作報錯

No value passed for parameter 'block'

然而,以上實作的curryingLike函數并不支援這樣做,因為它終究隻是Kotlin中的一種文法糖而已。它在函數調用形式上近似柯裡化的效果,但卻并不是柯裡化。Kotlin這樣設計的目的是讓我們采用最直覺熟悉的套路,來替代柯裡化實作這種簡潔的文法。

Scala中的corresponds方法是另一個典型的柯裡化應用,用它可以比較兩個序列是否在某個比對條件下相同。現在我們依靠Kotlin上面這種特殊的類柯裡化文法特性,再來實作一個Kotlin版本。

首先,我們先穿插下Kotlin的另一項新特性—擴充函數,這是Kotlin中十分強大的功能,我們會在第7章中重點介紹。目前我們先簡單了解下它的使用,因為corresponds方法需要借助它來實作。

簡單來說,Kotlin中的擴充函數允許我們在不修改已有類的前提下,給它增加新的方法。如代碼清單2-4所示。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

在這個例子中,類型View被稱為接收者類型,this對應的是這個類型所建立的接收者對象。this可以被省略,就像這樣子:

fun View.invisible() {

visibility = View.INVISIBLE           

我們給Android中的View類定義了一個invisible方法,之後View對象就可以直接調用該方法來隐藏視圖。

views.forEach { it.invisible() }

回到我們的corresponds方法,基于擴充函數的文法,我們就可以對Array類型增加這個方法。由于Kotlin的特殊文法支援,我們還是采用了定義普通多參數函數的形式。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

然後再用柯裡化的風格進行調用,就顯得非常直覺:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

雖然本節講述的是函數作為傳回值的應用,然而由于Kotlin的特殊文法,我們可以在大部分場景下用它來替代柯裡化的方案,顯得更加友善。

2.4 面向表達式程式設計

在本章之前的幾節中,我們已經好幾次與一個關鍵字打過交道,這就是“表達式”。現在羅列下我們已經提及的表達式概念:

  • if表達式
  • 函數體表達式
  • Lambda表達式
  • 函數引用表達式

顯然,表達式在Kotlin這門語言中處于一個相當重要的地位,這一節我們會着重介紹在Kotlin中如何利用各種表達式來增強程式表達、流程控制的能力。與Java等語言稍顯不同的是,Kotlin中的流程控制不再是清一色的普通語句,它們可以傳回值,是一些嶄新的表達式語句,如if表達式、when表達式、try表達式等。這樣的設計自然與表達式自身的特質相關。在了解具體的文法之前,我們先來探究下表達式和普通語句之間的差別。

表達式(expressions)和語句(statements)雖然是很基本的概念,但也經常被混淆和誤解。語句很容易了解,我們在一開始學習指令式程式設計的時候,程式往往是由一個個語句組成的。比如以下這個例子:

fun main(args: Array) {

var a = 1
while (a < 10) {
    println(a)
    a++
}           

可以看到,該程式依次進行了指派、循環控制、列印等操作,這些都可以被稱為語句。我們再來看看什麼是表達式:

表達式可以是一個值、常量、變量、操作符、函數,或它們之間的組合,程式設計語言對其進行解釋和計算,以求産生另一個值。

通俗地了解,表達式就是可以傳回值的語句。我們來寫幾個表達式的例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這些都是非常明顯的表達式。以下是Kotlin中更複雜的表達式例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

正如我們所言,一些在其他語言中的普通語句,在Kotlin中也可以是表達式。這樣設計到底有什麼好處呢?

2.4.1 表達式比語句更安全

我們先來寫一段Java代碼。剛開始我們還是采用熟悉的if語句用法:

void ifStatement(Boolean flag) {

String a = null;
if (flag) {
    a = "dive into kotlin";
}
System.out.println(a.toUpperCase());           

非常簡單的代碼,由于if在這裡不是一個表達式,是以我們隻能夠在外部對變量a進行聲明。仔細思考一下,這段代碼存在潛在的問題:

  • a必須在if語句外部聲明,它被初始化為null。這裡的if語句的作用就是對a進行指派,這是一個副作用。在這個例子中,我們忽略了else分支,如果flag的條件判斷永遠為true,那麼程式運作并不會出錯;否則,将會出現“java.lang.NullPointerException”的錯誤,即使程式依舊會編譯通過。是以,這種通過語句建立副作用的方式很容易引發bug。
  • 繼續思考,現在的邏輯雖然簡單,然而如果變量a來自上下文其他更遠的地方,那麼這種危險會更加容易被忽視。典型的例子就是一段并發控制的程式,業務開發會變得非常不安全。

接下來,我們再來建立一個Kotlin的版本,現在if會被作為表達式來使用:

fun ifExpression(flag: Boolean) {

val a = if (flag) "dive into Kotlin" else ""
println(a.toUpperCase())           

下面分析Kotlin的版本:

  • 表達式讓代碼變得更加緊湊了。我們可以把指派語句與if表達式混合使用,就不存在變量a沒有初始值的情況。
  • 在if作為表達式時,else分支也必須被考慮,這很容易了解,因為表達式具備類型資訊,最終它的類型就是if、else多個分支類型的相同類型或公共父類型。

可以看出,基于表達式的方案徹底消除了副作用,讓程式變得更加安全。當然,這并不是說表達式不會有副作用,實際上我們當然可以用表達式寫出帶有副作用的語句,就像這樣子:

var a = 1

fun foo() = if (a > 0) {

a = 2 // 副作用,a的值變化了
a           

} else 0

然而從設計角度而言,語句的作用就是服務于建立副作用的,相比較表達式的目的則是為了創造新值。在函數式程式設計中,原則上表達式是不允許包含副作用的。

一切皆表達式

撇開Haskell不談,在一些極力支援函數式程式設計的語言中,比如Scala和F#,即使它們不是純函數式語言,也都實作了一個特性,即一切皆表達式。一切皆表達式的設計讓開發者在設計業務時,促進了避免創造副作用的邏輯設計,進而讓程式變得更加安全。

由于把百分之百相容Java作為設計目标,Kotlin并沒有采納一切皆表達式的設計,然而它在Java的基礎上也在很大程度上增強了這一點。正如另一個接下來要提及的例子,就是Kotlin中的函數。與Java的函數不同,Kotlin中所有的函數調用也都是表達式。

2.4.2 Unit類型:讓函數調用皆為表達式

之是以不能說Java中的函數調用皆是表達式,是因為存在特例void。衆所周知,在Java中如果聲明的函數沒有傳回值,那麼它就需要用void來修飾。如:

void foo () {

System.out.println("return nothing");           

是以foo()就不具有值和類型資訊,它就不能算作一個表達式。同時,這與函數式語言中的函數概念也存在沖突,在Kotlin、Scala這些語言中,函數在所有的情況下都具有傳回類型,是以它們引入了Unit來替代Java中的void關鍵字。

void與Void

當你在描述void的時候,需要注意首字母的大小寫,因為Java在語言層設計一個Void類。java.lang.Void類似java.lang.Integer,Integer是為了對基本類型int的執行個體進行裝箱操作,Void的設計則是為了對應void。由于void表示沒有傳回值,是以Void并不能具有執行個體,它繼承自Object。

如何了解Unit?其實它與int一樣,都是一種類型,然而它不代表任何資訊,用面向對象的術語來描述就是一個單例,它的執行個體隻有一個,可寫為()。

那麼,Kotlin為什麼要引入Unit呢?一個很大的原因是函數式程式設計側重于組合,尤其是很多高階函數,在源碼實作的時候都是采用泛型來實作的。然而void在涉及泛型的情況下會存在問題。

我們先來看個例子,Java這門語言并不天然支援函數是頭等公民,我們現在來嘗試模拟出一種函數類型:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

看上去似乎沒什麼問題。我們再來改造下,這一次希望重新實作一個print方法。于是,難題出現了,Return的類型用什麼來表示呢?可能你會想到void,但Java中是不能這麼幹的。無奈之下,我們隻能把Return換成Void,即Function,由于Void沒有執行個體,則傳回一個null。這種做法嚴格意義上講,相當醜陋。

Java 8實際解決辦法是通過引入Action這種函數式接口來解決問題,比如:

  • Consumer,接收一個輸入參數并且無傳回的操作。
  • BiConsumer,接收兩個輸入參數的操作,并且不傳回任何結果。
  • ObjDoubleConsumer,接收一個object類型和一個double類型的輸入參數,無傳回值。
  • ObjIntConsumer,接收一個object類型和一個int類型的輸入參數,無傳回值。
  • ObjLongConsumer,接收一個object類型和一個long類型的輸入參數,無傳回值。
  • ……

雖然解決了問題,但這種方案不可避免地創造了大量的重複勞動,是以,最好的解決辦法就是引入一個單例類型Unit,除了不代表任何意義的以外,它與其他正常類型并沒有什麼差别。

2.4.3 複合表達式:更好的表達力

相比語句而言,表達式更傾向于自成一塊,避免與上下文共享狀态,互相依賴,是以我們可以說它具備更好的隔離性。隔離性意味着杜絕了副作用,是以我們用表達式描述邏輯可以更加安全。此外,表達式通常也具有更好的表達能力。

典型的一個例子就是表達式更容易進行組合。由于每個表達式都具有值,并且也可以将另一個表達式作為組成其自身的一部分,是以我們可以寫出一個複合的表達式。舉個例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這個程式描述了擷取一個HTTP響應結果,然後進行json解碼,最終指派給res變量的過程。它向我們展示了Kotlin如何利用多個表達式組合表達的能力:

  • try在Kotlin中也是一個表達式,try/catch/finally文法的傳回值類型由try或catch部分決定,finally不會産生影響;
  • 在Kotlin中,if-else很大程度上代替了傳統三元運算符的做法,雖然增加了文法詞數量,但是減少了概念,同時更利于閱讀;
  • if-else的傳回值即try部分的傳回值,最終res的值由try或catch部分決定。

Kotlin中的“?:”

雖然Kotlin沒有采用三元運算符,然而它存在一個很像的文法“?:”。注意,這裡的問号和冒号必須放在一起使用,它被叫作Elvis運算符,或者null合并運算符。由于Kotlin可以用“?”來表示一種類型的可空性,我們可以用“?:”來給一種可空類型的變量指定為空情況下的值,它有點類似Scala中的getOrElse方法。你可以通過以下的例子了解Elvis運算符:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

是不是覺得相當優雅?接下來,我們再來介紹Kotlin中when表達式,它比我們熟悉的switch語句要強大得多。

2.4.4 枚舉類和when表達式

本節主要介紹Kotlin中另一種非常強大的表達式—when表達式。在了解它之前,我們先來看看在Kotlin中如何定義枚舉結構,然後再使用when結合枚舉更好地來設計業務,并介紹when表達式的具體文法。

1.枚舉是類

在Kotlin中,枚舉是通過一個枚舉類來實作的。先來實作一個很簡單的例子:

enum class Day {

MON,
TUE,
WEN,
THU,
FRI,
SAT,
SUN           

與Java中的enum文法大體相似,無非多了一個class關鍵詞,表示它是一個枚舉類。不過Kotlin中的枚舉類當然沒那麼簡單,由于它是一種類,我們可以猜測它自然應該可以擁有構造參數,以及定義額外的屬性和方法。

enum class DayOfWeek(val day: Int) {

MON(1),
TUE(2),
WEN(3),
THU(4),
FRI(5),
SAT(6),
SUN(7)
;  // 如果以下有額外的方法或屬性定義,則必須強制加上分号

fun getDayNumber(): Int {
    return day
}           

需要注意的是,當在枚舉類中存在額外的方法或屬性定義,則必須強制加上分号,雖然你很可能不會喜歡這個文法。

枚舉類“分号”文法的由來

早期枚舉類的文法并沒有逗号,然而卻有點煩瑣:

enum class DayOfWeek(val day: Int) {

MON: DayOfWeek(1)
  TUE: DayOfWeek(2)
  WEN: DayOfWeek(3)
  THU: DayOfWeek(4)
  FRI: DayOfWeek(5)
  SAT: DayOfWeek(6)
  SUN: DayOfWeek(7)           

每個枚舉值都需要通過DayOfWeek(n)來構造,這确實顯得多餘。理想的情況是我們隻需調用MON(1)來表示即可。然而,簡化文法後也帶來了一些技術上的問題,比如在枚舉類源碼實作上很難把具體的枚舉值與類方法進行區分。解決這個問題有好幾種思路,第一種辦法就是把每個方法都加上一個注解字首,例如:

@inject fun getDayNumber(): Int {

return day           

但是這樣子就與其他的類在文法上顯得不一樣,破壞了文法的一緻性。好吧,那麼能不能反過來,給每個枚舉類弄個關鍵詞字首來區分,比如:

entry MON(1)

顯然,這樣也不好。因為枚舉值的數量無法控制,如果數量較多,會顯得啰唆。Kotlin最終采用了引入逗号和分号的文法,即通過逗号對每個枚舉值進行分隔,這樣就可以最終采用一個分号來對額外定義的屬性和方法進行隔離。

這确實是相對更合理的設計方案,尤其是加上逗号之後,Kotlin中的枚舉類文法跟Java的枚舉更相似了,這符合Kotlin的設計原則。

2.用when來代替if-else

在了解如何聲明一個枚舉類後,我們再來用它設計一個業務邏輯。比如,Shaw給新一周的幾天計劃了不同的活動,安排如下:

  • 周六打籃球
  • 周日釣魚
  • 星期五晚上約會
  • 平日裡如果天晴就去圖書館看書,不然就在寝室學習

他設計了一段代碼,利用一個函數結合本節最開頭的枚舉類Day來進行表示:

fun schedule(day: Day, sunny: Boolean) = {

if (day == Day.SAT) {
    basketball()
} else if (day == Day.SUN) {
    fishing()
} else if (day == Day.FRI) {
    appointment()
} else {
    if (sunny) {
        library()
    } else {
        study()
    }
}           

因為存在不少if-else分支,代碼顯得不夠優雅。對Kotlin日漸熟悉的Shaw開始意識到,更好的改進方法就是用when表達式來優化。現在我們來看看修改後的版本:

fun schedule(sunny: Boolean, day: Day) = when (day) {

Day.SAT -> basketball()
Day.SUN -> fishing()
Day.FRI -> appointment()
else -> when {
    sunny -> library()
    else -> study()
}           

整個函數一下子“瘦身”了很多,由于少了很多文法關鍵字幹擾,代碼的可讀性也更上了一層樓。

3. when表達式具體文法

我們根據上述這段代碼來分析下when表達式的具體文法:

1)一個完整的when表達式類似switch語句,由when關鍵字開始,用花括号包含多個邏輯分支,每個分支由->連接配接,不再需要switch的break(這真是一個惱人的關鍵字),由上到下比對,一直比對完為止,否則執行else分支的邏輯,類似switch的default;

2)每個邏輯分支具有傳回值,最終整個when表達式的傳回類型就是所有分支相同的傳回類型,或公共的父類型。在上面的例子中,假設所有活動函數的傳回值為Unit,那麼編譯器就會自動推導出when表達式的類型,即Unit。以下是一個非Unit的例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

3)when關鍵字的參數可以省略,如上述的子when表達式可改成:

when {

sunny -> library()
else -> study()           

該情況下,分支->的左側部分需傳回布爾值,否則編譯會報錯,如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

4)表達式可以組合,是以這是一個典型的when表達式組合的例子。你在Java中很少見過這麼長的表達式,但是這在Kotlin中很常見。如果你足夠仔細,還會看出這還是一個我們之前提到過的表達式函數體。

可能你會說,這樣嵌套子when表達式,層次依舊比較深。要知道when表達式是很靈活的,我們很容易通過如下修改來解決這個問題:

fun schedule(sunny: Boolean, day: Day) = when {

day == Day.SAT -> basketball()
day == Day.SUN -> fishing()
day == Day.FRI -> appointment()
sunny -> library()
else -> study()           

是不是很優雅?其實when表達式的威力遠不止于此。關于它更多的文法細節,我們會在第4章進一步介紹。同時你也将了解到如何利用when表達式結合代數資料類型,來對業務進行更好的抽象。

2.4.5 for循環和範圍表達式

在了解了Kotlin中的流程控制表達式之後,接下來就是我們熟悉的語句while和for。while和do-while的文法與在Java中并沒有大多的差異,是以我們重點來看下Kotlin中的for循環的文法和應用。

1. for循環

在Java中,我們經常在for加上一個分号語句塊來建構一個循環體,如:

for (int i = 0; i < 10; i++) {

System.out.println(i);           

在Kotlin中,表達上要更加簡潔,可以将上述的代碼等價表達為:

for (i in 1..10) println(i)

如果把上述的例子帶上花括号和變量i的類型聲明,也是支援的:

for (i: Int in 1..10) {

println(i)           

2.範圍表達式

你可能對“1..10”這種文法比較陌生,實際上這是在Kotlin中我們沒有提過的範圍表達式(range)。我們來看看它在Kotlin官網的文檔介紹:

Range表達式是通過rangeTo函數實作的,通過“..”操作符與某種類型的對象組成,除了整型的基本類型之外,該類型需實作java.lang.Comparable接口。

舉個例子,由于String類實作了Comparable接口,字元串值之間可以比較大小,是以我們就可以建立一個字元串區間,如:

"abc".."xyz"

字元串的大小根據首字母在字母表中的排序進行比較,如果首字母相同,則從左往右擷取下一個字母,以此類推。

另外,當對整數進行for循環時,Kotlin還提供了一個step函數來定義疊代的步長:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

如果是倒序呢?也沒有問題,可以用downTo方法來實作:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,還有一個until函數來實作一個半開區間:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

3.用in來檢查成員關系

另外一點需要了解的就是in關鍵字,在Kotlin中我們可以用它來對檢查一個元素是否是一個區間或集合中的成員。舉幾個例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

如果我們在in前面加上感歎号,那麼就是相反的判斷結果:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

除了等和不等,in還可以結合範圍表達式來表示更多的含義:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

以上的代碼等價于:

"kot" >= "abc" && "abc" <= "xyz"

事實上,任何提供疊代器(iterator)的結構都可以用for語句進行疊代,如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,我們還可以通過調用一個withIndex方法,提供一個鍵值元組:

for ((index, value) in array.withIndex()) {

println("the element at $index is $value")           

2.4.6 中綴表達式

本節中,我們已經見識了不少Kotlin中奇特的方法,如in、step、downTo、until,它們可以不通過點号,而是通過中綴表達式來被調用,進而讓文法變得更加簡潔直覺。那麼,這是如何實作的呢?

先來看看Kotlin标準庫中另一個類似的方法to的設計,這是一個通過泛型實作的方法,可以傳回一個Pair。

infix fun

A.to(that: B): Pair

在Kotlin中,to這種形式定義的函數被稱為中綴函數。一個中綴函數的表達形式非常簡單,我們可以了解成這樣:

A 中綴方法 B

不難發現,如果我們要定義一個中綴函數,它必須需滿足以下條件:

  • 該中綴函數必須是某個類型的擴充函數或者成員方法;
  • 該中綴函數隻能有一個參數;
  • 雖然Kotlin的函數參數支援預設值,但中綴函數的參數不能有預設值,否則以上形式的B會缺失,進而對中綴表達式的語義造成破壞;
  • 同樣,該參數也不能是可變參數,因為我們需要保持參數數量始終為1個。

函數可變參數

Kotlin通過varargs關鍵字來定義函數中的可變參數,類似Java中的“…”的效果。需要注意的是,Java中的可變參數必須是最後一個參數,Kotlin中沒有這個限制,但兩者都可以在函數體中以數組的方式來使用可變參數變量,正如以下例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

此外,我們可以使用*(星号)來傳入外部的變量作為可變參數的變量,改寫如下:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

由于to會傳回Pair這種鍵值對的結構資料,是以我們經常會把它與map結合在一起使用。如以下例子:

mapOf(

1 to "one",
2 to "two",
3 to "three"           

)

可以發現,中綴表達式的方式非常自然。接下來,我們再來自定義一個中綴函數,它是類Person中的一個成員方法:

class Person {

infix fun called(name: String) {
    println("My name is ${name}.")
}           

因為called方法用infix進行了修飾,是以我們可以這樣調用它:

val p = Person()
p called "Shaw"           

My name is Shaw.

需要注意的是,Kotlin仍然支援使用普通方法的文法習慣來調用一個中綴函數。如這樣來執行called方法:

p.called("Shaw")

然而,由于中綴表達式在形式上更像自然語言,是以之前的文法要顯得更加的優雅。

2.5 字元串的定義和操作

我們似乎破壞了一個傳統。根據慣例,每本程式設計語言的技術書開頭,似乎都會以列印一段“hello world!”的方式來宣告自己的到來。現在,我們決定秉承傳統,來完成這一任務。當然,此舉實際上不是為了宣揚某種儀式,而是因為本節的内容是關于Kotlin中又一項基礎的文法知識,也就是字元串操作。

Kotlin中的字元串并沒有什麼與衆不同,與Java一樣,我們通過雙引号來定義一個字元串,它是不可變的對象。

val str = "hello world!"

然後,我們可以對其進行各種熟悉的操作:

str.length // 12

str.substring(0,5) // hello

str + " hello Kotlin!" // hello world! hello Kotlin!

str.replace("world", "Kotlin") // hello Kotlin!

由于String是一個字元序列,是以我們可以對其進行周遊:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

還可以通路這個字元序列的成員:

str[0] // h

str.first() // h

str.last() // !

str[str.length - 1] // !

此外,Kotlin的字元串還有各種豐富的API,如:

// 判斷是否為空字元串

"".isEmpty() // true

" ".isEmpty() // false

" ".isBlank() // true

"abcdefg".filter { c -> c in 'a'..'d' } // abcd

更多字元串類方法可以查閱Kotlin API文檔:

https://kotlinlang.org/api/latest/jvm/stdlib/

kotlin/-string/index.html

2.5.1 定義原生字元串

Java在JEP 326改進計劃中提議,增加原生字元串的文法支援,因為目前它隻能通過轉義字元的迂回辦法來支援,非常麻煩。而在Kotlin中,已經支援了這種文法,我們來定義一個多行的原生字元串體驗一下:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

簡而言之,用這種3個引号定義的字元串,最終的列印格式與在代碼中所呈現的格式一緻,而不會解釋轉化轉義字元(正如上述例子中的n),以及Unicode的轉義字元(如uXXXX)。

比如,我們用字元串來描述一段HTML代碼,用普通字元串定義時必須是這樣子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

采用原生字元串的格式,會非常友善。如下:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

2.5.2 字元串模闆

我們再來舉一個很常見的字元串字面量與變量拼接的例子:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

上述代碼描述了一個消息模闆函數,通過傳入消息字段變量,最終傳回消息字元串。然而,簡簡單單的一句話,竟然使用了4個加号,可見相當地不簡潔。在Java中,這是我們經常會做的事情。

Kotlin引入了字元串模闆來改善這一情況,它支援将變量植入字元串。我們通過它來修改上面的message函數。

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

這與聲明一個普通的字元串在形式上沒什麼差別,唯一要做的就是把變量如姓名,通過${name}的格式傳入字元串。通過對比我們可以明顯看出,字元串模闆大大提升了代碼的緊湊性和可讀性。

此外,除了變量我們當然也可以把表達式通過同樣的方式插入字元串中,并且在${expression}中使用雙引号。如:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

2.5.3 字元串判等

Kotlin中的判等性主要有兩種類型:

  • 結構相等。通過操作符==來判斷兩個對象的内容是否相等。
  • 引用相等。通過操作符===來判斷兩個對象的引用是否一樣,與之相反的判斷操作符是!==。如果比較的是在運作時的原始類型,比如Int,那麼===判斷的效果也等價于==。

我們通過具體的例子來檢測下字元串兩種類型的相等性:

帶你讀《Kotlin核心程式設計》之二:基礎文法第2章

2.6 本章小結

(1)類型推導

Kotlin擁有比Java更加強大的類型推導功能,這避免了靜态類型語言在編碼時需要書寫大量類型的弊端。但它不是萬能的,在使用代碼塊函數體時,必須顯式聲明傳回值類型。此外,一些複雜情況如遞歸,傳回值類型聲明也不能省略。

(2)變量聲明

我們通過val和var在Kotlin中聲明變量,以及一些類的成員屬性,代表它們的引用可變性。在函數式開發中,我們優先推薦使用val和不可變對象來減少代碼中的副作用,提升程式的可靠性和可組合性。在一些個别情況下,尤其是強調性能的代碼中,用var定義局部變量會更加适合。

(3)函數聲明

在Kotlin中,一個普通的函數可分為代碼塊體和表達式體,前者類似于Java中我們定義函數的習慣,後者因為是一個表達式,是以可以省略return關鍵字。

(4)高階函數

因為擁抱函數式的設計,在Kotlin中函數是頭等公民,這不僅可以讓我們在程式中到處定義函數,同時也意味着函數可以作為值進行傳遞,以及作為另一個函數的傳回值。在函數作為參數的時候,我們需要使用函數引用表達式來進行傳值。柯裡化是函數作為傳回值的一種應用,然而在Kotlin中,由于特殊文法糖的存在,我們很少會使用柯裡化技術。

(5)Lambda表達式

Lambda是Kotlin中非常重要的文法特性,我們可以把它當作另一種匿名函數。Lambda簡潔的文法以及Kotlin語言深度的支援,使得它在我們用Kotlin程式設計時得到極大程度的應用。

(6)表達式和流程控制

表達式在Kotlin中有着相當重要的地位,這與表達式本身相較于普通語句所帶來的優勢有關。與後者相比,表達式顯得更加安全,更有利于組合,且擁有更強的表達能力。在Kotlin中,流程控制不像Java是清一色的普通語句,利用if、when、try、range、中綴等表達式我們能夠寫出更加強大的代碼。與此同時,Kotlin中的for語句也要更加精簡。

(7)字元串操作

Kotlin的字元串跟Java一樣,定義的都是不可變對象。除了提供更多豐富的字元串API之外,Kotlin還支援原生字元串、字元串模闆這些Java目前并沒有支援的特性。