Kotlin實(shí)戰(zhàn)02 — Kotlin基礎(chǔ)

這章將講述

  • 聲明函數(shù)、變量、類、枚舉和屬性
  • Kotlin的控制結(jié)構(gòu)
  • 智能強(qiáng)轉(zhuǎn)
  • 拋和處理異常

1 基本元素: 函數(shù)和變量

Kotlin的兩大元素:函數(shù)和變量。你將明白,你可以省略類型聲明,還有鼓勵(lì)你使用不變而不是可變的數(shù)據(jù)。

1.1 Hello, world!

讓我們以一個(gè)經(jīng)典的例子開始:打印“Hello, world!”,在Kotlin中,你只需要一個(gè)函數(shù):

fun main(args: Array<String>) { 
    println("Hello, world!") 
}

在這段簡短的代碼中,我們可以觀察到什么該語言的什么部分和特點(diǎn)呢?請(qǐng)看下面的列表:

  • fun關(guān)鍵詞用來聲明一個(gè)函數(shù)
  • 參數(shù)類型現(xiàn)在參數(shù)名字的后面。同樣適用于變量聲明
  • 函數(shù)可以在文件的最上層中聲明;你沒必要把它放到一個(gè)類中
  • 數(shù)列僅僅是類。不像Java,Kotlin沒有特定的聲明數(shù)組的語法。
  • 用println,而不是System.out.println。Kotlin標(biāo)準(zhǔn)庫提供了很多標(biāo)準(zhǔn)Java庫函數(shù)的包裝,這有更簡潔的語法。println就是其中之一。
  • 在一行的最后省略了分號(hào),就像在其他的語言。

1.2 函數(shù)

如果函數(shù)有返回類型,在函數(shù)參數(shù)后面加冒號(hào)和返回類型:

fun max(a: Int, b: Int): Int { 
    return if (a > b) a else b 
}
println(max(1, 2)) //2

注意,在Kotlin中if是個(gè)有返回值的表達(dá)式。類似于Java中的三目運(yùn)算符(a > b)? a : b

語句(statement)和表達(dá)式(expression)

在Kotlin中,if是個(gè)表達(dá)式,而不是一個(gè)語句。語句和表達(dá)式的區(qū)別在于,表達(dá)式是一個(gè)值,可以被用作另外表達(dá)式的一部分;而語句總是一個(gè)包含它的代碼塊內(nèi)的頂層元素,沒有自己的值。在Java中,所有的控制結(jié)構(gòu)都是語句,但是在Kotlin中,大部分控制結(jié)構(gòu),除了循環(huán)(for , do和do/while),是表達(dá)式。聯(lián)合控制結(jié)構(gòu)和其他的表達(dá)式,可以讓你簡潔表達(dá)許多通常的模式。

另外一方面,在Java中賦值是表達(dá)式,但是在Kotlin中變成了語句。這有效避免了比較和賦值之間的混淆,這個(gè)混淆也是錯(cuò)誤的一個(gè)來源。

表達(dá)式主體
進(jìn)一步可以簡化前面函數(shù),因?yàn)楹瘮?shù)體只含有單個(gè)語句,你可以用表達(dá)式來作為整個(gè)函數(shù)體,移除花括號(hào)和返回語句:

fun max(a: Int, b: Int): Int = if (a > b) a else b

如果用花括號(hào)來表達(dá)函數(shù)主體,我們叫這個(gè)函數(shù)為代碼塊體(block body),如果直接返回表達(dá)式,我們叫它為表達(dá)式體(expression body)。

INTELLIJ IDEA提示 IntelliJ IDEA提供了在兩種不同函數(shù)風(fēng)格“Convert to expression body”和 “Convert to block body”之間的轉(zhuǎn)換

表達(dá)式體的函數(shù)在Kotlin代碼中很常見,這個(gè)風(fēng)格不止用在一行函數(shù),也用在對(duì)單一和更加復(fù)雜的表達(dá)式求值,比如if,when和try。我們進(jìn)一步省略返回類型:

fun max(a: Int, b: Int) = if (a > b) a else b

為什么函數(shù)沒有返回類型的聲明呢?Kotlin不是一個(gè)靜態(tài)語言,要求每個(gè)表達(dá)式在編譯階段都有類型嗎?事實(shí)上,每個(gè)變量和每個(gè)表達(dá)式都有類型,每個(gè)函數(shù)也有返回類型。但是對(duì)于表達(dá)式體的函數(shù),編譯器可以分析作為函數(shù)體的表達(dá)式,用它的類型作為返回類型,即使沒有顯示的寫出來。分析的這個(gè)類型通常叫類型推導(dǎo)(type inference)。

注意,省略返回類型僅僅在表達(dá)試體的函數(shù)中允許。有代碼塊體的有返回值的函數(shù),你必須指明返回類型和顯示的返回語句。這是個(gè)有意的抉擇。實(shí)際中的函數(shù)通常非常長,可能包含很多返回語句,有顯示的返回類型和語句可以幫助你快速的知道什么被返回。

1.3 變量

在Java中,你用類型聲明變量。但是在Kotlin中,你可以也可以不把類型放到變量名后面。省略類型的聲明如下

val question = "The Ultimate Question of Life, the Universe, and Everything"
val answer = 42

或者你顯示的指明

val answer: Int = 42

如果你要浮點(diǎn)型的常量,可以推導(dǎo)為Double

val yearsToCompute = 7.5e6//7.5X10^6 = 7500000.0

可變和不可變量

  • val(來源于value)--- 不變的引用。一旦聲明為val的量初始化后,不能夠重新賦值。對(duì)應(yīng)于Java里面的final變量
  • var(來源于variable)--- 可變的引用。變量的值可以改變。對(duì)應(yīng)于Java里面的正常的變量(非final)

通常,盡量聲明所有的變量為val關(guān)鍵詞。只有有需要的時(shí)候,才變?yōu)関al。用不可變的引用、不可變的實(shí)例和函數(shù),沒有副作用,使得你的代碼更像函數(shù)式的風(fēng)格。val變量只能在代碼塊中初始化有且僅有一次。但是可以根據(jù)不同的情況,用不同的值來初始化,如果編譯器能夠保證僅有一個(gè)初始化語句執(zhí)行:

val message: String
if (canPerformOperation()) {
    message = "Success"
    // ... perform the operation } 
else {
    message = "Failed" 
}

注意,val引用自己是不可變的,但是,他指向的實(shí)例是可以改變的。比如,下面的代碼是完全有效的:

val languages = arrayListOf("Java") //聲明不可變的引用
languages.add("Kotlin")//改變引用指向的實(shí)例

盡管var關(guān)鍵詞允許變量改變他的值,但是它的類型是確定的:

var answer = 42 
answer = "no answer"http://編譯錯(cuò)誤:類型不匹配

如果你想在變量里面存儲(chǔ)一個(gè)不匹配的類型的值,你必須轉(zhuǎn)換或者協(xié)變這個(gè)值到正確的類型。

1.4 更容易的字符串格式化:字符串模板

這個(gè)部分開始的“Hello World”例子,我們進(jìn)一步這個(gè)慣例,用Kotlin的方式,通過名字來問候。

fun main(args: Array<String>) { 
    //打印“Hello,Kotlin”,如果輸入?yún)?shù)為Bob,則打印“Hello,Bob”
    val name = if (args.size > 0) args[0] else "Kotlin" 
    println("Hello, $name!") 
}

這個(gè)例子引進(jìn)了一個(gè)功能叫字符串模板(string templates)。和其他腳本語言一樣,Kotlin允許在字符串字面量中,通過$字符放在變量名前面,引用本地變量。這個(gè)同Java中的字符串連接("Hello, " + name + "!"), 但是更加緊湊和有效率(注:都是創(chuàng)建StringBuilder,添加常量部分和變量值,Java虛擬機(jī)有優(yōu)化)。

如果你引用一個(gè)不存在的本地變量,因?yàn)楸磉_(dá)式會(huì)靜態(tài)檢查,這些代碼會(huì)編譯不成功。如果你想在字符串中包含$符號(hào),用println("\$x")換碼,打印出$x,而不是把x翻譯為一個(gè)變量的引用。

不限于一個(gè)簡單的變量名,你也可以用更加復(fù)雜的表達(dá)式,僅僅只要在表達(dá)式括上花括號(hào):

fun main(args: Array<String>) { 
    //用${}插入args數(shù)組的第一個(gè)元素
    if (args.size > 0) { println("Hello, ${args[0]}!") } 
}

你也可以雙引號(hào)內(nèi)陷雙引號(hào),只要他們是在同一個(gè)表達(dá)式:

fun main(args: Array<String>) { 
    println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

2 類和屬性

讓我們看看一個(gè)簡單的JavaBean的Person類,現(xiàn)在只包含一個(gè)name屬性

/* Java */ public class Person { 
private final String name;
    public Person(String name) { 
        this.name = name;     
    }
    public String getName() { 
        return name; 
    }
}

在Java中,構(gòu)造子的代碼塊內(nèi),常常包含一些重復(fù)內(nèi)容:把參數(shù)賦值到響應(yīng)的域。在Kotlin中,這個(gè)邏輯不需要如此多的樣板代碼。在第一章中5.6節(jié),我們介紹了Java-to-Kotlin轉(zhuǎn)換器:一個(gè)自動(dòng)把Java代碼轉(zhuǎn)換到Kotlin代碼的工具,代碼的功能是相同的。

class Person(val name: String)

如果你熟悉現(xiàn)代的JVM語言,你可能見過類似的東西。這個(gè)類型的類(只包含數(shù)據(jù),但沒有代碼),常常叫值實(shí)例(value object),許多語言提供了聲明他們的簡潔語法。注意到在轉(zhuǎn)換過程中,public修飾符不見了。因?yàn)樵贙otlin,public是默認(rèn)的可見性,所以你能省略它

2.1 屬性

你肯定知道,類的概念是封裝數(shù)據(jù)和處理數(shù)據(jù)的代碼到單一的實(shí)體。在Java中,數(shù)據(jù)存儲(chǔ)到域中,通常是私有的。如果你想讓類的客戶端訪問這個(gè)數(shù)據(jù),你需要提供訪問器方法(accessor meth-ods):一個(gè)getter、可能有一個(gè)setter。你在Person類的例子中已經(jīng)看到。setter可能包含一些額外的邏輯,驗(yàn)證傳遞值,或者發(fā)送值變化的通知等等。

在Java中,域和訪問器的組合,通常叫做屬性(property), 很多框架較多使用這個(gè)概念。在Kotlin中,屬性是語言支持的第一等功能,完全用來替代域和它的訪問器方法。就像你用val和var關(guān)鍵詞,定義一個(gè)變量,你可以同樣的方式定義類的屬性。聲明為val的屬性是只讀的,而var屬性是可變的,

class Person( 
    val name: String, //只讀屬性:自動(dòng)生成一個(gè)域和簡單的getter
    var isMarried: Boolean //可寫屬性:一個(gè)域,getter和setter
)

基本上,當(dāng)你定一個(gè)屬性,你就定義了相應(yīng)的訪問器。默認(rèn)地,定義訪問器也是簡單的,域存儲(chǔ)值、getter和setter來返回和更新值。如果你愿意,用不同的邏輯計(jì)算和更新屬性值,來自定義訪問器。上面的Person簡潔的定義隱藏了實(shí)現(xiàn)的細(xì)節(jié),就像原來的Java代碼一樣:一個(gè)類含有私有的域并且在構(gòu)造子中初始化,可以用響應(yīng)的getter獲取到。這意味著,你可以在Java和Kotlin中使用這個(gè)類,不管這個(gè)類在哪里申明的。使用是一樣的。下面是Java代碼中如何使用:

/* Java */
Person person = new Person("Bob", true);
System.out.println(person.getName()); //Bob
System.out.println(person.isMarried()); //true

Kotlin的name屬性在Java中的getter方法叫g(shù)etName。getter和setter命名規(guī)則有個(gè)例外:如果屬性名以is開始,getter沒有附加的前綴,在setter名字中,is被set取代。所以,在Java中,你調(diào)用isMarried()。如下是Kotlin的結(jié)果

val person = Person("Bob", true)
println(person.name)// Bob
println(person.isMarried) //true

你不是調(diào)用getter,而是直接引用屬性。邏輯是一樣的,但是代碼更加簡潔。可變屬性的setter一樣,在java中你用person.setMarried(false)表達(dá)離婚,在Kotlin中person.isMarried = false。

提示 你可以在Java定義的類中使用Kotlin的屬性語法。在Java類中的getter可以在Kotlin中val屬性獲取,getter/setter可以通過var屬性獲取。比如,如果在Java類定義了setName和setName的方法,那么可以通過叫name的屬性獲取。如果類定義了isMarried和setMarried方法,相應(yīng)的Kotlin屬性叫isMarried。

大多數(shù)情況下,屬性有相應(yīng)的支持屬性,即存儲(chǔ)屬性值。但是如果值是隨手計(jì)算的,比如從其他屬性計(jì)算,你可以用自定義的getter表達(dá)。

2.2 自定義訪問器

這個(gè)部分,你將看到怎么自定義實(shí)現(xiàn)一個(gè)屬性訪問器。假設(shè)你聲明了一個(gè)長方形,它可以告訴是不是一個(gè)正方形。你沒必要用單獨(dú)的域存儲(chǔ)這個(gè)信息,因?yàn)槟阈枰獎(jiǎng)討B(tài)檢查高是否等于寬:

class Rectangle(val height: Int, val width: Int) { 
    val isSquare: Boolean 
    get() { //Property getter declaration
        return height == width
    } 
}

isSquere屬性不需要一個(gè)域來存儲(chǔ)它的值。它僅僅只有自定義實(shí)現(xiàn)的getter。這個(gè)屬性被獲取時(shí)每次計(jì)算。注意到,你不需要用花括號(hào)這個(gè)完整的語法,你可有寫成get() = height == width。這樣的屬性的調(diào)用也是一樣的:

val rectangle = Rectangle(41, 43)
println(rectangle.isSquare) //false

如果你想在Java中獲取這個(gè)屬性,你可以就像以前一樣調(diào)用isSquare方法。

你可能問,是否定義一個(gè)沒有參數(shù)的函數(shù)比自定義getter的屬性好。這兩個(gè)選項(xiàng)是相似的:在實(shí)現(xiàn)和性能是沒有區(qū)別的,它們僅僅在可讀性上有差別。一般講,如果你描述一個(gè)類的特點(diǎn)(屬性),你應(yīng)該定義它為屬性。

2.3 Kotlin源碼布局:目錄和包

Java把所有的類放進(jìn)包里面。Kotlin也像Java,有包的概念。每個(gè)Kotlin文件在開頭有package語句,文件中所有的聲明(類、函數(shù)和屬性)將放在這個(gè)包下。如果其他的文件在同一包下,里面所有的定義可以直接使用;如果這些定義在不同包里面,那么他們需要導(dǎo)入。就像在Java中,導(dǎo)入語句放置在文件的開頭,使用import關(guān)鍵詞。下面是個(gè)例子,展示包聲明和導(dǎo)入語句:

package geometry.shapes //包聲明

import java.util.Random //導(dǎo)入標(biāo)準(zhǔn)Java庫類

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width 
} 

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt()) 
}

Kotlin不會(huì)區(qū)別導(dǎo)入類和函數(shù),允許你用import關(guān)鍵詞導(dǎo)入任何聲明。你可以通過名字導(dǎo)入頂層函數(shù)

package geometry.example 

import geometry.shapes.createRandomRectangle //通過名字導(dǎo)入函數(shù)

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)//非常不可能打印"true"
}   

通過包名后面加上.*,你可以導(dǎo)入特定包里面定義的所有聲明。注意,星號(hào)導(dǎo)入(star import)不僅使得定義包里面的類可見,而且使得頂層函數(shù)和屬性可見。在上面的例子中,import geometry.shapes.* 代替顯示的導(dǎo)入,也使得使得代碼正常編譯。

在Java中,目錄結(jié)構(gòu)和包的層級(jí)是重復(fù)的。在Kotlin中你可以在同個(gè)文件中定義多個(gè)類。Kotlin也沒限制磁盤上源文件的結(jié)構(gòu)。你可以用目錄結(jié)構(gòu)來組織你的文件。比如,你可以在文件shapes.kt中定義geometry.shapes包的所有內(nèi)容,然后把這個(gè)文件放在geometry目錄下,沒有必要?jiǎng)?chuàng)建shapes文件夾。


package hierarchy

但是,在大多數(shù)情況下,跟隨Java目錄結(jié)構(gòu)和根據(jù)包結(jié)構(gòu)把源碼組織成目錄,是最佳實(shí)踐。特別是Kotlin和Java混合的項(xiàng)目,堅(jiān)持這樣的結(jié)構(gòu)特別重要。因?yàn)檫@樣做可以讓你逐步遷移代碼,而沒有引入意外的情況。但是請(qǐng)你不要猶豫把多個(gè)類合成到同一個(gè)文件,特別是當(dāng)類很小的時(shí)候(在Kotlin中,這些經(jīng)常存在)。

3 選項(xiàng)的表述和處理:枚舉和“when”

在這一節(jié)中,我們將講述when結(jié)構(gòu)。它可以被想成Java中的switch替代品,但是更加強(qiáng)大和更常使用。同時(shí),有一個(gè)在Kotlin中聲明枚舉的例子,然后討論智能強(qiáng)轉(zhuǎn)的概念。

3.1 聲明枚舉類

讓我們加一些想象的明亮照片到這個(gè)嚴(yán)肅的書籍,看看下面顏色的枚舉

enum class Color { 
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET 
}

與Java的僅僅有關(guān)鍵詞enum相比,Kotlin聲明使用更多的關(guān)鍵詞,這是比較少見的。在Kotlin中,enum就是所謂的軟關(guān)鍵詞(soft keyword):當(dāng)它放置在class關(guān)鍵詞之前,它才有特有的意義。但是你可以在其他的地方,把它當(dāng)成常規(guī)的名字使用。另外一方面,class還是一個(gè)關(guān)鍵詞,你可以用clazz和aClass來聲明變量。
就像在Java中,枚舉不是值的列表:你可以在枚舉類中聲明屬性和方法:

enum class Color( 
    val r: Int, val g: Int, val b: Int //聲明枚舉常量的屬性
) {
    RED(255, 0, 0), //當(dāng)每個(gè)變量創(chuàng)建的時(shí)候,指定屬性值
    ORANGE(255, 165, 0), //逗號(hào)是必須的
    YELLOW(255, 255, 0), 
    GREEN(0, 255, 0), 
    BLUE(0, 0, 255),
    INDIGO(75, 0, 130), 
    VIOLET(238, 130, 238);
    fun rgb() = (r * 256 + g) * 256 + b//定義枚舉的方法
}
println(Color.BLUE.rgb())//255

枚舉常量就像你看見的正常的類一樣,有相同的構(gòu)造子和屬性聲明。當(dāng)你定義一個(gè)枚舉常量,你需要為它提供屬性值。這個(gè)例子中展示了Kotlin語法唯一需要分號(hào)的地方:在枚舉類中如果你定義任何方法,分號(hào)區(qū)分了枚舉常量列表和方法聲明。

3.2 用“when”來處理枚舉類

你記得孩子如何利用助記短語來記憶彩虹的顏色嗎?這就是一個(gè):“Richard Of York Gave Battle In Vain!”假設(shè)你需要一個(gè)函數(shù)來給你為每個(gè)顏色一個(gè)助記(你不想把這些信息存儲(chǔ)在枚舉里面)。在Java中,你使用switch語句,在Kotlin中,響應(yīng)的結(jié)構(gòu)是when。

就像if一樣,when也是一個(gè)返回值的表達(dá)式,所以你可以寫一個(gè)函數(shù),它的表達(dá)式體直接返回when表達(dá)式。如下:

fun getMnemonic(color: Color) = //直接返回一個(gè)“when”的表達(dá)式
    when (color) { //如果顏色等于枚舉常量,返回響應(yīng)的字符串
        Color.RED -> "Richard" 
        Color.ORANGE -> "Of" 
        Color.YELLOW -> "York" 
        Color.GREEN -> "Gave" 
        Color.BLUE -> "Battle" 
        Color.INDIGO -> "In" 
        Color.VIOLET -> "Vain"
    }
}
println(getMnemonic(Color.BLUE)) // Battle

不像Java,你不需要為每個(gè)分支寫break語句(缺少break是Java代碼中引入錯(cuò)誤的原因)。你可以在同個(gè)分支結(jié)合值,用逗號(hào)來分離:

fun getWarmth(color: Color) = when(color) { 
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm" 
    Color.GREEN -> "neutral" 
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold" 
}
 println(getWarmth(Color.ORANGE)) //warm

上面的例子用全名來使用枚舉常量,指定Color枚舉類名。你可以用導(dǎo)入常量來簡化代碼:

import ch02.colors.Color //導(dǎo)入聲明在另外一個(gè)包的Color類
import ch02.colors.Color.*//用名字顯示導(dǎo)入枚舉常量

fun getWarmth(color: Color) = when(color) { 
    RED, ORANGE, YELLOW -> "warm" //用名字導(dǎo)入常量
    GREEN -> "neutral" 
    BLUE, INDIGO, VIOLET -> "cold"
}

3.3 使用任意實(shí)例的“when”

Java中的switch,需要使用常量(枚舉常量、字符串或者數(shù)字字面常量)作為分支條件,但是在Kotlin中,when允許任何的實(shí)例。下面我們寫一個(gè)混合兩者顏色的函數(shù),如果它們可以在這個(gè)小的調(diào)色板中能夠混合。

fun mix(c1: Color, c2: Color) = 
    when (setOf(c1, c2)) {//when表達(dá)式的參數(shù)可以是任何實(shí)例,用來被分支條件檢查
        setOf(RED, YELLOW) -> ORANGE//枚舉可以混合的顏色對(duì)
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO 
        else -> throw Exception("Dirty color")//執(zhí)行這個(gè),如果沒有分支可以匹配
    }
println(mix(BLUE, YELLOW))//GREEN

Kotlin標(biāo)準(zhǔn)庫中含有一個(gè)setOf的函數(shù),用來創(chuàng)建Set,包含參數(shù)指定的實(shí)例;一個(gè)set是一個(gè)集合,它的項(xiàng)的次序并不重要。所以,如果setOf(c1, c2)和setOf(RED, YELLOW)是相等的,那么意味著要不然c1是RED和c2是YELLOW,或者相反。

3.4 用沒有參數(shù)的when

上面的例子有點(diǎn)效率低下,因?yàn)槊看文阏{(diào)用這個(gè)函數(shù),它都會(huì)創(chuàng)建幾個(gè)Set實(shí)例,僅僅是用在檢查兩個(gè)顏色是否匹配另外兩個(gè)顏色。正常情況下,通常不是個(gè)問題。但是如果這個(gè)函數(shù)經(jīng)常被調(diào)用,那么為了避免GC,值得用另外一種方式來重寫這個(gè)代碼。你可以用不帶參數(shù)的when表達(dá)式完成。代碼雖然可讀性差一點(diǎn),但是這是為了達(dá)到更好性能付出的代價(jià)。

fun mixOptimized(c1: Color, c2: Color) = 
    when { 
        (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
        (c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
        (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
        else -> throw Exception("Dirty color")
    } 
println(mixOptimized(BLUE, YELLOW)) //GREEN

如果when表達(dá)式?jīng)]有參數(shù),它的分支可以是任何的布爾值。mixOptimized函數(shù)和上面的mix做相同的事情。優(yōu)點(diǎn)是不需要?jiǎng)?chuàng)建任何額外的實(shí)例,但是代價(jià)是更難閱讀。

3.5 智能強(qiáng)轉(zhuǎn):結(jié)合類型檢查和強(qiáng)轉(zhuǎn)

作為整個(gè)部分的例子,寫一個(gè)簡單算術(shù)表達(dá)式的求值,比如(1+2)+4。其他的算術(shù)操作(減乘除)也可以用類似的方式實(shí)現(xiàn),你可以當(dāng)做一個(gè)練習(xí)。
第一, 你怎么編碼這個(gè)表達(dá)式?你可以在樹狀結(jié)構(gòu)中存儲(chǔ),每個(gè)節(jié)點(diǎn)是一個(gè)總數(shù)(Sum)或者一個(gè)數(shù)字(Num)。Num永遠(yuǎn)是葉子節(jié)點(diǎn),而Sum節(jié)點(diǎn)有兩個(gè)作為sum操作的參數(shù)的子節(jié)點(diǎn)。以下列表顯示的是一個(gè)用來編碼表達(dá)式的類的簡單結(jié)構(gòu):Expr的接口,它的Num和Sum兩個(gè)實(shí)現(xiàn)類。需要注意的是,Expr沒有聲明任何方法,僅僅是用作標(biāo)記接口,提供相同類型的不同種類表達(dá)式。

interface Expr 
class Num(val value: Int) : Expr 
class Sum(val left: Expr, val right: Expr) : Expr

Sum存儲(chǔ)了左邊和右邊Expr類型的參數(shù)的引用。在這個(gè)小例子中,它們可以使Num或者Sum。為了存儲(chǔ)表達(dá)式(1+2)+4,你可以穿件一個(gè)實(shí)例:Sum(Sum(Num(1), Num(2)), Num(4))。如下圖展示的樹狀結(jié)構(gòu)


樹狀表達(dá)式

我們看看怎么計(jì)算一個(gè)表達(dá)式的值:

println (eval(Sum(Sum(Num(1), Num(2)), Num (4)))) //7

Expr接口有兩個(gè)實(shí)現(xiàn),所以對(duì)于一個(gè)表達(dá)式求最終的值,你可以有兩種選擇:

  • 如果表達(dá)式是一個(gè)數(shù)字,返回相應(yīng)的值
  • 如果是一個(gè)和,對(duì)左邊和右邊表達(dá)式求值,返回它們的和

首先,我們用通常的Java方式來寫這個(gè)函數(shù),然后用kotlin的方式重構(gòu)。在Java中,你可能用if語句序列來檢查選項(xiàng),所以讓我們?cè)贙otlin中用同樣的方法:

fun eval(e: Expr): Int { 
    if (e is Num) { 
        val n = e as Num //顯式的強(qiáng)轉(zhuǎn)到Num是冗余的
        return n.value 
    } if (e is Sum) { 
        return eval(e.right) + eval(e.left) //變量e是智能強(qiáng)轉(zhuǎn)
    } 
    throw IllegalArgumentException("Unknown expression")
}
println(eval(Sum(Sum(Num(1), Num(2)), Num(4)))) //7

在Kotlin中,檢查一個(gè)變量是不是某種類型用is檢查。如果你在C#編程過,這個(gè)概念會(huì)很熟悉。is檢查和Java中的instanceOf類似。但是在Java中,如果你已經(jīng)檢查了一個(gè)變量是否是某種類型,同時(shí)想取得這個(gè)類型的屬性,你需要在instanceOf檢查后面再加一個(gè)顯式的類型轉(zhuǎn)換。初始的變量需要不只使用一次,通常需要把類型轉(zhuǎn)換后的變量存儲(chǔ)到單獨(dú)的變量中。在kotlin中,編輯器為你做了這些工作。如果你檢查變量為某種類型,你沒必要在后面再類型轉(zhuǎn)換。你可以當(dāng)做你想檢查的類型來使用它。事實(shí)上,編譯器,編譯為我們類型轉(zhuǎn)換了,我們叫它只能智能類型轉(zhuǎn)換(smart cast)

在eval函數(shù)中,在你檢查這個(gè)變量e是否是Num類型后,編譯器解釋它為Num變量。然后你可以不需要顯式的類型轉(zhuǎn)換就可以取得Num的value屬性。同樣的情況適用于Sum的右邊和左邊的屬性: 在相應(yīng)的情形下你只需要寫e.right和e.left。在IDE中,智能轉(zhuǎn)換的值用一個(gè)背景色來強(qiáng)調(diào),所以你可以很容易知道這個(gè)值是預(yù)先檢查了的,如下:


image

智能轉(zhuǎn)換只有變量在is檢查后沒有被改變。當(dāng)你對(duì)一個(gè)類的屬性進(jìn)行智能轉(zhuǎn)換,屬性必須是val,而且不能有自定義的存取器。否則不能確定每次獲得這個(gè)屬性將獲得同樣的值。一個(gè)顯式轉(zhuǎn)換到特定類型用as關(guān)鍵詞來表達(dá):

val n = e as Num

3.7 代碼塊作為if和when的分支

if和when都可以用代碼塊作為分支。在這個(gè)例子中,代碼塊中最后最后一個(gè)表達(dá)式作為結(jié)果。如果你想在例子函數(shù)中加日志,你可以在代碼塊中完成,并用最后一個(gè)值返回。

fun evalWithLogging(e: Expr): Int =
    when (e) { 
        is Num -> { 
            println("num: ${e.value}") 
            e.value //如果e是Num類型,這是代碼塊最后一個(gè)表達(dá)式,并被返回
        } 
        is Sum -> { 
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right) 
            println("sum: $left + $right") 
            left + right//如果表達(dá)式被返回當(dāng)e是Sum類型
        } 
        else -> throw IllegalArgumentException("Unknown expression")
    }
    println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4)))) 
    //num: 1 
    //num: 2 
    //sum: 1 + 2 
    //num: 4 
    //sum: 3 + 4 
    //7

“代碼塊中最后一個(gè)表達(dá)式是返回值”這個(gè)規(guī)則,在使用代碼塊而且期待返回一個(gè)結(jié)果的情況西安,所有情形下都成立。你將在本章結(jié)束的時(shí)候,同樣的規(guī)則在try代碼體和catch子句下同樣適用。在第五章論述lambada表達(dá)式下它的應(yīng)用。但是在2.2節(jié)中提到,這個(gè)規(guī)則對(duì)于常規(guī)的函數(shù)式不適用的。一個(gè)函數(shù)可以是沒有代碼塊的表達(dá)式體,或者是一個(gè)有顯示的return語句的代碼塊體

4 迭代事物:while和for循環(huán)

for循壞只存在一種形式,相對(duì)于Java的for-each循環(huán)。就像C#里面的寫法:for <item> in <elements>。不存在Java中的通常for語法。

4.1 while循環(huán)

和Java對(duì)應(yīng)的循環(huán)一樣的語法

while (condition) { //當(dāng)條件為真時(shí),代碼體執(zhí)行
    /*...*/ 
}
do {//無條件的執(zhí)行一次,之后當(dāng)條件為真時(shí)執(zhí)行
    /*...*/ 
} while (condition)

4.2 數(shù)字的迭代:范圍和累進(jìn)

由于不存在Java中通常的for語法,Kotlin用范圍(ranges)這個(gè)概念。范圍是兩個(gè)值之間的間距,這兩個(gè)值為開始和結(jié)束的值,用..操作子表示。

val oneToTen = 1..10

范圍在Kotlin是自閉的(Closed)或者自包含(inclusive),這意味著第二個(gè)值總是范圍的一部分。如果你迭代范圍內(nèi)的所有的值,這樣的范圍也叫累進(jìn)(progression)

讓我們用整數(shù)的范圍玩Fizz-Buzz游戲。參與者輪流遞增式數(shù)數(shù),用fizz單詞替代任何可以被三整除的數(shù)字,用buzz單詞替代任何可以被五整除的數(shù)字。如果一個(gè)數(shù)字同時(shí)是三和五的乘數(shù),我們叫“FizzBuzz”。

如下列表打印了從1到100之間的正確答案。注意這么用沒有參數(shù)的when表達(dá)式檢查可能的條件:

fun fizzBuzz(i: Int) = when { 
    i % 15 == 0 -> "FizzBuzz " //i可以被15整除,返回FizzBuzz。就像在Java中,%是模操作
    i % 3 == 0 -> "Fizz " //i可以被5整除,返回Buzz
    i % 5 == 0 -> "Buzz " //i可以被3整除,返回Fizz
    else -> "$i " //Else返回這個(gè)數(shù)字本身
}
for (i in 1..100) { //迭代整數(shù)范圍1..100
    print(fizzBuzz(i))
}
//1 2 Fizz 4 Buzz Fizz 7 ...

如果你覺得厭倦了這些規(guī)則,想要把規(guī)則搞的復(fù)制一些。讓我們從100倒過來數(shù),而且只包括偶數(shù):

for (i in 100 downTo 1 step 2) { 
    print(fizzBuzz(i)) 
}
//Buzz 98 Fizz 94 92 FizzBuzz 88 ...

當(dāng)你以一個(gè)步長step迭代一個(gè)累進(jìn),這可以忽略一些數(shù)字。這個(gè)步長可以是負(fù)數(shù),這樣的話累進(jìn)向后而不是向前。這這個(gè)例子中,100 downTo 1 是一個(gè)(步長為-1)向后的累進(jìn)。然后步長改變它的絕對(duì)值為2,同時(shí)保持方向(事實(shí)上是,設(shè)置步長為-2).

就像前面提到的,..語法創(chuàng)建了一個(gè)包含終點(diǎn)(..右邊的值)的一個(gè)范圍。在許多情況下,迭代半自閉的范圍,即不包含指定的終點(diǎn),這樣會(huì)更加方便。為了創(chuàng)建這樣一個(gè)范圍,用until函數(shù)實(shí)現(xiàn)。比如,for (x in 0 until size)循環(huán)等于for (x in 0..size-1),但是它表達(dá)的意思更加清楚。

4.3 map的迭代

我們提到過,追常見的情形是,for...in循環(huán)是迭代一個(gè)集合。這個(gè)是和Java是一樣的,所以毋庸贅言。下面我們看看怎么迭代一個(gè)map。

舉個(gè)例子,讓我看看一個(gè)小程序,打印字符的二進(jìn)制表示。僅僅為了展示的目的,你將存儲(chǔ)二進(jìn)制表示到一個(gè)map之中。下面的代碼創(chuàng)建了一個(gè)map,用一些字母的二進(jìn)制填進(jìn)去,然后打印map里面的內(nèi)容。

val binaryReps = TreeMap<Char, String>()//用TreeMap,所以鍵是排序的

for (c in 'A'..'F') { //用字符的范圍迭代從A到F的字符
    val binary = Integer.toBinaryString(c.toInt()) //ASCII編碼轉(zhuǎn)換到二進(jìn)制
    binaryReps[c] = binary//在map中用c鍵存儲(chǔ)值
}
for ((letter, binary) in binaryReps) { //迭代一個(gè)map,把鍵值對(duì)賦值到兩個(gè)變量
    println("$letter = $binary")
}

..語法創(chuàng)建范圍不僅僅對(duì)數(shù)字適用,也對(duì)字符適用。我們用它迭代所有的字符,從A到(包括)F。

上面顯示了,for循環(huán)讓你解構(gòu)迭代集合的元素,在這個(gè)例子中,是map里面鍵值對(duì)的集合。解構(gòu)的結(jié)果存儲(chǔ)到兩個(gè)不同值中:letter接受鍵,而binary接受值。另外一個(gè)有用的技巧是,用鍵獲取和更新一個(gè)map里面的值。不是調(diào)用get和put,你用map[key]讀值,而用map[key] = value設(shè)置。代碼binaryReps[c] = binary相當(dāng)于Java里面的binaryReps.put(c, binary)。輸出如下:

A = 1000001 B = 1000010 C = 1000011 D = 1000100 E = 1000101 F = 1000110

你可有用同樣的結(jié)構(gòu)語法迭代一個(gè)集合,同時(shí)記錄當(dāng)前項(xiàng)的索引。你不必手動(dòng)的創(chuàng)建一個(gè)獨(dú)立的存儲(chǔ)索引的變量。編碼打印如你所料,如下:

val list = arrayListOf("10", "11", "1001") 
for ((index, element) in list.withIndex()) { 
    println("$index: $element") 
}
//0: 10 
//1: 11 
//2: 1001

4.4 用in檢查集合和范圍的屬性

用in操作子檢查一個(gè)值是否在范圍里面,或者想法,用!in檢查是否一個(gè)值是否不在一個(gè)范圍里面。下面看看怎么用in檢查一個(gè)字符是否屬于字符范圍里面:

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' 
fun isNotDigit(c: Char) = c !in '0'..'9'
println(isLetter('q')) //true
println(isNotDigit('x')) //true

背下地,沒有任何詭計(jì):檢查字符編碼是否在第一個(gè)和最后一個(gè)編碼之間的某個(gè)地方。但是這個(gè)邏輯被隱藏在標(biāo)準(zhǔn)庫里面范圍類的實(shí)現(xiàn)里面。

c in 'a'..'z'//變換成a <= c && c <= z

in和!in操作子也可以在when表達(dá)式里面使用

fun recognize(c: Char) = when (c) { 
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!" 
    else -> "I don't know…"
}
println(recognize('8')) //It's a digit!

范圍也不限于字符。如果你有任何支持比較實(shí)例的類(實(shí)現(xiàn)java.lang.Comparable接口),你可以創(chuàng)建那種類型實(shí)例的范圍。如果你有這樣的范圍,你不能枚舉這個(gè)范圍的所有的實(shí)例。想想這個(gè):比如,你能枚舉在“Java”和“Kotlin”之間的所有的字符串嗎?是的,你不能。但是你依然可以用in操作子檢查另外實(shí)例是否屬于這個(gè)范圍:

println("Kotlin" in "Java".."Scala") //和“Java” <= “Kotlin” && “Kotlin” <= “Scala”一樣
//true

字符串在這里是按字母比較的,因?yàn)槟鞘荢tring類怎么實(shí)現(xiàn)Comparable接口的。同樣的in檢查對(duì)集合也適用:

println("Kotlin" in setOf("Java", "Scala")) //這個(gè)集沒有“Kotlin”字符串
//false

5 Kotlin中的Exception

Kotlin中的異常處理與Java或者其他語言中的處理方式相似。一個(gè)函數(shù)可以以正常方式結(jié)束,或者當(dāng)錯(cuò)誤發(fā)生的時(shí)候拋出異常。函數(shù)調(diào)用者捕獲這個(gè)異常并處理它;如果沒有,異常重新在調(diào)用棧向上拋。

Kotlin中的異常處理語句的基本形式和Java是相似的。你可以以不足為奇的方式拋出一個(gè)異常:

if (percentage !in 0..100) { 
    throw IllegalArgumentException( "A percentage value must be between 0 and 100: $percentage") 
}

就像其他的類,你不需要用new關(guān)鍵詞創(chuàng)建異常實(shí)例。不像Java,在Kotlin中,throw結(jié)構(gòu)是一個(gè)表達(dá)式,可以用作為其他表達(dá)式的一部分:

val percentage = 
    if (number in 0..100) 
        number
    else 
        throw IllegalArgumentException( //“throw” 是一個(gè)表達(dá)式
            "A percentage value must be between 0 and 100: $number")

5.1 try、catch和finally

就像Java之中,可以用try結(jié)構(gòu),和catch和finally子句處理異常。如下,讀取指定文件的一行,嘗試解析為個(gè)數(shù)字,然后返回一個(gè)數(shù)字,如果這行不是有效的數(shù)字,返回null。

fun readNumber(reader: BufferedReader): Int? { //不必要顯式地指定需要這個(gè)函數(shù)拋出的異常
    try { 
        val line = reader.readLine() 
        return Integer.parseInt(line) 
    } catch (e: NumberFormatException) { //異常的類型在右邊
        return null 
    } finally { //finally就像在Java一樣的
        reader.close()
    }
}
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
//239

在段代碼和Java最大的不同是不需要throws子句:如果你在Java中寫這個(gè)函數(shù),你必須顯式地在函數(shù)聲明后面寫throws IOException。你必須這么做是因?yàn)镮OException是受檢查的異常checked exception。在Java中,一個(gè)異常必須顯式的處理。你不得不聲明函數(shù)可以拋的所有的受檢查異常。如果你調(diào)用其他的函數(shù),你需要處理它的受檢查的異常,或者聲明你的函數(shù)拋出這些異常。
就像其他現(xiàn)代JVM語言,Koltin不區(qū)別受檢查和不受檢查的異常。你需要指定一個(gè)函數(shù)拋出的異常,你可以也可以不處理這些異常。這個(gè)設(shè)計(jì)決定是基于Java中使用受檢查異常的實(shí)踐。經(jīng)驗(yàn)表明,Java規(guī)則常常需要很多無意義的代碼從新拋出或者忽略異常,這個(gè)規(guī)則并不能一致地避免發(fā)生的錯(cuò)誤。

在上面的例子中,NumberFormatException是一個(gè)不受檢查的異常。所以Java編譯器不會(huì)強(qiáng)迫你捕獲這個(gè)異常,你可以很容易的看見運(yùn)行時(shí)的異常。這相當(dāng)令人遺憾,因?yàn)椴挥行У妮斎霐?shù)據(jù)是經(jīng)常的事情,應(yīng)該更優(yōu)雅的處理。同時(shí),BufferedReader.close方法也能拋出一個(gè)IOException異常,這是個(gè)需要處理的受檢查的異常。如果關(guān)閉一個(gè)流失敗了,大部分代碼不能采取任何有意義的行動(dòng),所以需要從close方法捕獲異常的代碼基本是樣板代碼。

那么關(guān)于Java 7的try-with-resources怎么樣呢?Kotlin沒有對(duì)應(yīng)的特別的語法;它被處理成一個(gè)庫函數(shù)。

5.2 try作為一個(gè)表達(dá)式

為了顯示Java和Kotlin直接一個(gè)重要區(qū)別,讓我們稍微改變下這個(gè)例子。移除fianlly部分(因?yàn)槟阋呀?jīng)知道這個(gè)怎么工作),然后加一些代碼打印從這個(gè)文件讀取的數(shù)字。

fun readNumber(reader: BufferedReader) {
    val number = try { 
        Integer.parseInt(reader.readLine()) //成為try表達(dá)式的值
    } catch (e: NumberFormatException) {
        return 
    } 
    println(number)
}

val reader = BufferedReader(StringReader("not a number"))
readNumber(reader)//沒有打印任何數(shù)字

Kotlin中try關(guān)鍵詞,就像if和when,引進(jìn)了一個(gè)表達(dá)式,你可以把它的值賦值給一個(gè)變量。不像if,你一直需要把語句保函在花括號(hào)中。就像其他語句,如果包涵多個(gè)表達(dá)式,try表達(dá)式的值是最后一個(gè)表達(dá)式的值。在這個(gè)例子中,在catch代碼塊中有return語句,所以這個(gè)函數(shù)在catch代碼塊后不會(huì)再進(jìn)行。如果你想繼續(xù)這個(gè)執(zhí)行,catch語句也需要一個(gè)值,這個(gè)值是最后表達(dá)式的值:

fun readNumber(reader: BufferedReader) {
    val number = try { 
        Integer.parseInt(reader.readLine()) //沒有異常發(fā)生時(shí)使用這個(gè)值
    } catch (e: NumberFormatException) {
        null //異常發(fā)生時(shí)使用null值
    }
    println(number)
}
val reader = BufferedReader(StringReader("not a number"))
readNumber(reader)//異常被拋出,所以函數(shù)打印null
//null

這時(shí)候如果你不耐煩了,你可以用類似Java中的寫法,開始在Kotlin中寫代碼。當(dāng)你讀這本書的時(shí)候,你將繼續(xù)學(xué)習(xí)怎么改變你習(xí)慣的思考方式,使用這個(gè)新語言的全部功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容