Kotlin系統化學習-20170823
文章中有大部分內容是借鑒《Kotlin實戰》中文版,特此注明一下。
Kotlin系統化系列學習文章(有待更新):
http://blog.csdn.net/ClAndEllen/article/details/77466628
前篇文章我們把Kotlin的背景,學習流程整理了,以及詳細說明了Kotlin語言的一些優勢,接下來進入正式的學習:
K02-Kotlin基礎
本篇文章學習內容:
1.聲明函數,變量,類,枚舉以及屬性。
2.Kotlin中的控制結構。
3.智能轉換。
4.拋出和處理異常。
2.1 函數和變量
2.1.1 Hello,World!
fun main(args: Array<String>) {
println("Hello,World!")
}//相當于Java中的main,只不過這個main方法不放在一個單獨的類中,而是放于頂層
你能從這樣簡單的一小段代碼中觀察到哪些特性和語法?
1.關鍵字fun用來聲明一個函數。沒錯,Kotlin編程有很多樂趣(fun)。
2.參數類型寫在名稱后面。變量的聲明也是如此。
3.函數可以定義在文件的最外層(頂層),不需要把它放在類中。
4.使用println代替了System.out.println。Kotlin標準庫給Java標準庫函數提供了許多語法更簡潔的包裝,而println就是其中一個。
5.和許多其他現代語言一樣,可以省略每行代碼結尾的分號。
2.1.2 函數
你已經看到了怎樣聲明一個沒有任何返回任何東西的函數。但是如果函數有一個有意義的結果,返回類型應該放在哪里呢?其實很簡單就放置與參數列表之后,緊跟于參數列表之后,以“:”號隔開,示例代碼如下:
fun max(a:Int,b:Int):Int{
return if(a>=b) a else b
}//普通寫法
一個函數的基本結構如下圖2.1所示:
語句和表達式:
在Kotlin中,if是表達式,而不是語句。語句和表達式的區別在于,表達式有值,并且能作為另一個表達式的一部分使用;而語句總是包圍著它的代碼塊中的頂層元素,并且沒有自己的值。在Java中,所有的控制結構都是語句。而在Kotlin中,除了循環(for,do和do/while)以外大多數控制結構都是表達式。這種結合控制結構和其他表達式的能力讓你可以簡明扼要地表示許多常見的模式,稍后你會在本書中看到這些內容。
??另一方面,Java中的賦值操作是表達式,在Kotlin中反而變成了語句。這有助于避免比較和賦值之間的混淆,而這種混淆是常見的錯誤來源。
表達式函數體
在Kotlin中,有一種函數的寫法,讓函數的聲明變得非常簡單,因為它的函數體是單個表達式構成,這種寫法去掉了return和“{}”花括號,比如上面的代碼寫成表達式函數的形式如下:
fun max(a:Int,b:Int):Int = if(a>=b) a else b
如果函數體寫在花括號中,我們說這個函數有代碼塊體。如果它直接返回了一個表達式,它就有表達式體。上述代碼還可以這么簡化,把返回類型省略掉:
fun max(a:Int,b:Int) = if(a>=b) a else b
為什么有些函數可以不聲明返回類型?作為一門靜態類型語言,Kotlin不是要求每個表達式都應該在編譯期具有類型嗎?事實上,每個變量和表達式都有類型。每個函數都有返回類型。但是對表達式函數來說,編譯器會分析作為函數體的表達式,并把它的類型作為函數的返回類型,即使沒有顯式地寫出來。這種分析通常被稱作類型推導。
??注意,只有表達式體函數的返回類型可以省略。對于有返回值的代碼塊體函數,必須顯式地寫出返回類型和return語句。這是刻意的選擇。真實項目中的函數一般很長且可以包含多條return語句,顯式地寫出返回類型和return語句能幫助你快速地理解函數能返回的是什么。接下來我們看看聲明變量的語法。
2.1.3 變量
在Java中聲明變量的時候會以類型開始。在Kotlin中這樣是行不通的,因為許多變量聲明的類型都可以省略。所以在Kotlin中以關鍵字開始,然后是變量名稱,最后可以加上類型(不加也可以):
val question =
"The Ultimate Question of Life,the Universe, and Everything"
val answer = 42
這個例子省略了類型聲明,但是如果需要也可以顯式地指定變量的類型:
val answer:Int = 42
和表達體函數一樣,如果你不指定變量的類型,編譯器會分析初始化器表達式的值,并把它的類型作為變量的類型。在前面這個例子中,變量的初始化器42的類型是Int,那么變量就是這個類型。你可以理解編譯器很聰明,總是給變量推導出合適的類型。
??如果變量沒有初始化器,需要顯式地指定它的類型:如果不能提供可以可以賦給這個變量的值的信息,編譯器就無法推斷出它的類型。
可變量和不可變量
聲明變量的關鍵字有兩個:
val(來自value)---不可變引用。使用val聲明的變量不能在初始化之后再次賦值。它對應的是Java的final的變量。
var(來自variable)---可變引用。這種變量的值可以被改變。這種聲明對應的是普通(非final)的Java變量。
默認情況下,應該盡可能地使用val關鍵字來聲明所有的Kotlin變量,僅在必要的時候換成var。使用不可變引用,不可變對象及無副作用的函數讓你的代碼更接近函數式編程風格。只要你對Java中的“final”理解夠透徹,理解Kotlin中的val也就不在話下。我這里就不再啰嗦了。var雖然是可變的,但是前提必須是類型匹配的,比如以下代碼:
var a = 3 //這里已經確定a的類型為Int
a = 5
a = "3" //報錯,因為a的類型是不可變的
2.1.4 更簡單的字符串格式化:字符串模版
Java當中的字符串模版很死板,變量的輸出必須由"+"號連接,卻不能鑲嵌于一個常規的String當中,比如輸出Student類的age和name,Java是這么輸出的:
System.out.println("學生姓名:"+s.name+",學生年齡:"+s.age);
要是輸出Student類的屬性有很多,那么就有點繁瑣了,Kotlin就避免這一麻煩,可以在字符串字面值中引用局部變量,只需要在變量名稱前面加上$,比如下面的代碼:
var a = 5
println("a的值是:$a")
當然如果要輸出一段表達式或者是引用調用某個屬性,函數等,那么需要給輸出的區域添加上花括號,示例代碼如下:
fun main(args: Array<String>) {
var a1 = 5
var a2 = 6
val s = Student("ellen",23)
println("相加的的值是:${a1+a2}") // 輸出表達式
println("學生姓名:${s.name},學生年齡:${s.myName()}")//輸出通過引用調用屬性或者方法
}
class Student(name:String,age:Int){
var name = ""
var age = 0
fun myName() = name
}
現在你知道了如何定義函數和變量,那么接下來,來看看類。這一次,你會借助Java到Kotlin的轉換器來開始運用新的語言特性。
2.2 類和屬性
面向對象對你來說可不是什么新鮮的概念,你也許非常熟悉類的抽象機制。Kotlin這方面的概念你也會覺得似曾相識,但是你會發現許多常見的任務使用更少的代碼就可以完成。這一節會向你介紹聲明類的基本語法,在后面在深入細節。
??首先,來看一個簡單的JavaBean類Person,目前它只有一個屬性,name。
Java代碼是這樣的:
private class Person{
private final String name;
public Person(String name){
this.name = name;
}
public String getName(){
return name;
}
}
在Java中,構造方法的方法體常常包含完全重復的代碼:它把參數賦值給有著相同名稱的字段。在Kotlin中,這種邏輯不用這么多的樣板代碼就可以表達。后面介紹Java到Kotlin的轉換器:一個把Java代碼替換成功能相同的Kotlin代碼的工具。把上述代碼使用轉換器轉換的代碼如下:
class Person(val name:String)
就是這么強悍,如果你試過其他的一些現代JVM語言,你也許見過類似的事情。這中類(只有數據沒有其他代碼)通常被叫作值對象,許多語言都提供簡明語法來聲明它們。
??注意從Java到Kotlin的轉換過程中public修飾符消失了。在Kotlin中public是默認的可見性,所以你能省略它。
2.2.1 屬性
你肯定知道,類的概念就是把數據和處理數據的代碼封裝成一個單一的實體。在Java中,數據存儲在字段中,通常還是私有的。如果想讓類的使用者訪問到數據,得提供訪問器方法:一個getter,可能還有一個setter。在Person類中你已經看到了訪問器的例子。setter還可以包含額外的邏輯,包括驗證傳給它的值。發送關于變化的通知等。
??在Java中,字段和其訪問器的組合常常被叫作屬性,而許多框架嚴重依賴這個概念。在Kotlin中,屬性是頭等的語言特性,完全代替了字段和訪問器方法。在類中聲明一個屬性和聲明一個變量一樣:使用val和var關鍵字。聲明成val的屬性是只讀的,而var屬性是可變的。
class Person{
val name:String, //只讀屬性:生成一個字段和一個簡單的getter
var isMarried:Boolean //可見屬性:一個字段,一個getter和一個setter
}
基本上,當你聲明屬性的時候,你就聲明了對應的訪問器(只讀屬性只有一個getter,而可寫屬性既有getter,也有setter)。訪問器的默認實現非常簡單:創建一個存儲值的字段,以及返回值的getter和更新值的setter。但是如果有需要,可以聲明自定義的訪問器,使用不同的邏輯來計算和更新屬性的值。
Person person = new Person("ellen",false);
System.out.println(person.getName());
System.out.println(person.isMarried());
注意,不管Person是定義在Java還是Kotlin中,這段代碼看起來是一樣的。Kotlin的屬性name把一個名稱getName的getter方法暴露給Java。getter和setter的命名規則有一個例外:如果屬性的名稱以is開頭,getter不會增加任何的前綴;而它的setter名稱中的is會被替換成set。所以在Java中,你強調的將是isMarried()。
??將上述Java代碼使用Person的代碼換成Kotlin代碼如下:
val person = Person("ellen",true) //調用構造方法不需要關鍵字“new”
//可以直接訪問屬性,但是調用的是getter
println(person.name)
println(person.isMarried())
現在,可以直接引用屬性,不再需要調用getter。邏輯沒有變化,但代碼更簡潔了。可變屬性的setter也是這樣:在Java中,使用person.setMarried(false)來表示離婚,而在Kotlin中,可以這樣寫person.isMarried = false 。
小貼士 對于那些在Java中定義的類,一樣可以使用Kotlin的屬性語法。Java類中的getter可以被當成val屬性在Kotlin中訪問,而一對getter/setter可以被var屬性訪問。例如,如果一個Java類定義了兩個名稱為getName和setName的方法,就把它們當作名稱為name的屬性訪問。如果類定義了isMarried和setMarried方法,對應的Kotlin屬性的就是isMarried。
大多數情況下,屬性有一個對應的支持字段來保存屬性的值。但是如果這個值可以即時計算---例如,根據其他屬性計算---可以自定義的getter來表示。
2.2.2 自定義訪問器
這一節將向你展示怎樣寫一個屬性訪問器的自定義實現。假設你聲明這樣一個矩形。它能判斷自己是否是正方形。不需要一個單獨的字段來存儲這個信息(是否是正方形),因為可以隨時通過檢查矩形的長寬是否相等來判斷:
class Rectangle(val height:Int,val width:Int){
val isSquare:Boolean
get(){
return height == width
}
}
屬性isSquare不需要字段來保存它的值。它只有一個自定義實現的getter。它的值時每次訪問屬性的時候計算出來的。
??注意,不需要使用帶花括號的完整語法,也可以這樣寫get( ) = height == width。對這個屬性的調用依然不變。
val rectangle = Rectangle(41,43)
println(rectangle,isSquare)//輸出false
如果在Java中訪問這個屬性,可以像前面提到的那樣調用isSquare()的方法。
??你可能會問,聲明一個沒有參數的函數是否比聲明帶自定義getter的屬性更好。兩種方式幾乎一樣:實現和性能都沒有差別,唯一的差異時可讀性。通常來說,如果描述的是類的特征(屬性),應該把它聲明成屬性。接下來我們來探索一下Kotlin的代碼在磁盤中說怎樣組織的。
2.2.3 Kotlin源碼布局:目錄和包
你知道Java把所有的類組織成包。Kotlin也有和Java相似的包的概念。每一個Kotlin文件都能以一條package語句開頭,而文件中定義的所有聲明(類,函數及屬性)都會放在這個包中。如果其他文件中定義的聲明也有相同的包,這個文件可以直接使用他們;如果包不相同,則需要導入它們。和Java一樣,導入語句放在文件的最前面并使用關鍵字import。下面這個源碼文件的例子展示了包聲明和導入語句的語法。
package geometry.shapes
import java.util.Random
class Rectangle(val height:Int,val width:Int){
val isSquare:Boolean
get() = height == width
}
fun createRandomRectangle():Rectangle{
val random = Random()
return Rectangle(random.nexInt(),random.nexInt())
}
Kotlin不區分導入的是類還是函數,而且,它允許使用import關鍵字導入任何種類的聲明。可以直接導入頂層函數的名稱。
package geometry.example
import geometry.shapes.createRandomRectangle
fun main(args:Array<String>){
println(createRandomRectangle().isSquare)
}
也可以在包名稱后加上.來導入特定包中定義的所有聲明。注意這種星號導入不僅讓包中定義的類可見,也會讓頂層函數和屬性可見。在上述代碼中用import geometry.shapes.的寫法代替顯式的導入也能讓代碼成功編譯。
??在Java中,要把類放到和包結構相匹配的文件與目錄結構中。例如,如果你有一個包含若干類的名為shapes的包,必須把每一個類都放在一個有著和類相同名字的單獨文件中,然后把這些文件放在一個名字為shapes的目錄中。圖2.2展示了geometry包以及它的子包在Java中上是怎樣組織的。假設createRandomRectangle函數位于另外一個單獨的類RectangleUtil。
在Kotlin中,可以把多個類放在同一個文件中,文件的名字還可以隨意選擇。Kotlin也沒有對磁盤上源文件的布局強加任何限制。比如,可以把包geometry.shapes所有內容都放在文件shapes.kt中,并把這個文件直接放在目錄geometry中,而不需要在創建一個獨立的shapes文件夾(如下圖所示)
example.kt --> 包geometry.example
shapes.kt --> 包geometry.example
不管怎樣,大多數情況下,遵循Java的目錄布局并根據包結構把源碼文件放到目錄中,依然是個不錯的實踐。在Kotlin和Java混用的項目中堅持這樣的結構尤為重要,因為這樣做可以讓你逐步地遷移代碼,而不會和一些錯誤不期而遇。但是你應該毫不猶豫地把多個類放進同一個文件中,特別是那些很小的類(在Kotlin中,類通常很小)。接下來,來學習Kotlin控制結構。
2.3 表示和處理選擇:枚舉和“when”
2.3.1 聲明枚舉類
Kotlin中的枚舉類聲明說和Java是差不多的,下面我們就來看看Kotlin中的枚舉,下面來看看使用Kotlin來進行色彩枚舉類的聲明:
enum class Color{
RED,ORANGE,YELLOW,GREEN,BLUE,INDIGO,VIOET
}
這是極少數Kotlin聲明比Java使用了更多關鍵字的例子:Kotlin用了enum class兩個關鍵字,而Java只有enum一個關鍵字。Kotlin中,enum是一個所謂的軟關鍵字:只有當它出現在class前面才有特殊的意義,在其他地方可以把它當作普通的名稱使用。與此不同的是,class仍然是一個關鍵字,要繼續使用名稱clazz和aClass來聲明變量。
??和Java一樣,枚舉并不是值的列表:可以給枚舉聲明屬性和方法。下面的代碼清單展示了這種方式。
fun main(args: Array<String>) {
println(Color.BLUE.rgb()) //輸出:255
}
enum class Color(val r:Int,val g:Int,val b:Int){
RED(255,0,0),ORANGE(255,165,0),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
}
枚舉常量用的聲明構造方法和屬性的語法與之前你看到的常規類一樣。當你聲明每個枚舉常量的時候,必須提供該變量的屬性值。注意這個例子向你展示Kotlin語法中唯一必須使用分號的地方:如果要在枚舉類中定義任何方法,就要使用分號把枚舉常量列表和方法定義分開。現在我們來看看一些在代碼中處理枚舉常量的超酷的方式。
2.3.2 使用“when”處理枚舉類
和if相似,when是一個有返回值的表達式,因此可以寫一個直接返回when表達式的表達式體函數。
fun main(args: Array<String>) {
println(getColorString(Color.INDIGO)) //輸出:INDIGO
}
fun getColorString(color:Color) = when(color){//Java中的switch在Kotlin中使用when表達式實現
Color.RED->"RED"
Color.ORANGE->"ORANGE"
Color.YELLOW->"YELLOW"
Color.GREEN->"GREEN"
Color.BLUE->"BLUE"
Color.INDIGO->"INDIGO"
Color.VIOLET->"VIOLET"
}//非合并when分支
fun getColorString1(color:Color) = when(color){
Color.RED, Color.ORANGE->"RED OR ORANGE"http://合并分支
Color.YELLOW, Color.GREEN, Color.BLUE->"YELLOW GREEN BLUE"http://合并分支
Color.INDIGO->"INDIGO"
Color.VIOLET->"VIOLET"
}
enum class Color(val r:Int,val g:Int,val b:Int){
RED(255,0,0),ORANGE(255,165,0),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
}
如果你還覺得上面代碼when表達式不夠簡潔,那么你可以通過導入枚舉常量的方式來簡化when表達式:
import geometry.Color
import geometry.Color.*
fun main(args: Array<String>) {
println(getColorString(BLUE))
}
fun getColorString(color:Color) = when(color){
RED->"RED"
ORANGE->"ORANGE"
YELLOW->"YELLOW"
GREEN->"GREEN"
BLUE->"BLUE"
INDIGO->"INDIGO"
VIOLET->"VIOLET"
}
2.3.3 在“when”結構中使用任意對象
Kotlin中的when結構比Java中的Switch強大很多。Switch要求必須使用常量(枚舉常量,字符串或者數字字面值)作為分支條件,和它不一樣,when允許使用任意對象。我們寫一個函數來混合兩種顏色,如果它們在我們這個小小的調色板是能夠混合的。你只有很少的選項,可以簡單地把所有組合列舉出來。
fun mix(c1:Color,c2:Color){
when(setOf(c1,c2)){//setof()方法返回一個set集合
setOf(RED,YELLOW)->ORANGE
setOf(YELLOW,BLUE)->GREEN
setOf(BLUE,VIOLET)->INDIGO
else->throw Exception("Dirty color")//如果沒匹配其它分支,就會執行此處
}
}
enum class Color(val r:Int,val g:Int,val b:Int){
RED(255,0,0),ORANGE(255,165,0),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
}
when表達式把它的實參依次和所有分支匹配,直到某個分支滿足條件。這里setOf(c1,c2)被用來檢查是否和分支條件相等:先和setOf(RED,YELLOW)比較,然后是其他顏色的set,一個接一個。如果沒有其他的分支滿足條件,else分支會執行。
??能使用任何表達式做when的分支條件,很多情況下會讓你寫的代碼既簡潔又漂亮。這個例子中,分支條件是等式檢查,接下來你會看到條件還可以是任意的布爾表達式。
2.3.4 使用不帶參數的“when”
你可能注意到上述代碼的效率多少有些低。每次調用這個函數的時候,它都會創建一些Set實例,僅僅用來檢查兩種給定的顏色是否和另外兩種顏色匹配。一般這不是什么大問題,但是如果這個函數調用很頻繁,它就會非常值得用另外一種方式重寫,來避免創建額外的垃圾對象。代碼可讀性會變差,但這是為了達到更好性能而必須付出的代價。
fun mix(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")
}
}
enum class Color(val r:Int,val g:Int,val b:Int){
RED(255,0,0),ORANGE(255,165,0),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
}
如果沒有給when表達式提供參數,分支條件就是任意的布爾表達式。mixOptimized函數和前面的mix函數做的事情一樣。這種寫法的有點就是不會創建額外的對象,但代價是它更難理解。
2.3.5 智能轉換:合并類型檢查和轉換
你會寫一個函數來作為這一小節的例子,這個函數是對象(1+2)+4這樣簡單的算術表達式求值。這個表達式只包含一種運算:對兩個數字求和。其他的算術運算(減法,乘法,除法)都可以用相似的方式實現,可以把這些作為練習。
??首先,你會用怎樣的形式編碼這種表達式?把它們存儲在一個樹狀結構中,結構中每個節點要么是一次求和(Sum)要么是一個數字(Num)。Num永遠都是葉子節點,而Sum節點有兩個子節點:它們是求和運算的兩個參數。下面的代碼展示了一種簡單的類結構來表示這種表達式編碼方式:一個叫作Expr的接口和它的兩個實現類Num和Sum。注意Expr接口沒有聲明任何方法,它只是一個標記接口,用來給不同種類的表達式提供一個公共的類型。聲明類的時候,使用一個冒號(:)后面跟上接口名稱,來標記這個類實現了這個接口。
interface Expr
class Num(var value:Int):Expr //簡單的值對象類,只有一個屬性value,實現了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum運算的實參可以是任何Expr:Num或者另外一個Sum
Sum存儲了Expr類型的實參left和right的引用;在這個小例子中,它們要么是Num要么是Sum。為了存儲前面提到的表達式(1+2)+4,你會創建這樣一個對象Sum(Sum(Num(1),Num(2)),Num(4))。下圖展示了它的樹狀圖表示法。
現在我們來看看怎樣計算表達式的值。例子中表達式的運算結果應該是7。
Expr接口有兩種實現,所以為了計算出表達式的結果值,得嘗試兩種選項:
??1.如果表達式是一個數字,直接返回它的值。
??2.如果是一次求和,得先計算左右兩個表達式得值。再返回它們的和。
首先我們來看看這個函數用普通的Java方式怎么寫,然后我們把它重構成Kotlin風格寫法。在Java中,很可能會用一連串if語句來檢查這些選項,所以我們先用Kotlin按照這種方式實現:
interface Expr
class Num(var value:Int):Expr //簡單的值對象類,只有一個屬性value,實現了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum運算的實參可以是任何Expr:Num或者另外一個Sum
fun eval(e:Expr):Int{
if(e is Num){
val n = e as Num
return n.value
}
if(e is Sum){
return eval(e.left)+ eval(e.right)
}
throw IllegalArgumentException("Unknown expression")
}
在Kotlin中,你要使用is檢查來判斷一個變量是否是某種類型。如果你曾經使用過C#寫過代碼,這種表示法應該不會陌生。is檢查和Java中的instanceof相似。但是Java中,如果你已經檢查過一個變量是某種類型并且把它當作這種類型來訪問其成員時,在instanceof檢查之后還需要顯式地加上類型轉換。如果最初的變量會使用超過一次,常常選擇把類型轉換的結果存儲在另外一個單獨的變量里。在Kotlin中,編譯器幫你完成這些工作。如果你檢查過一個變量是某種類型,后面就不需要再轉換它,可以就把它當作你檢查過的類型使用。事實上編譯器為你執行了類型轉換,我們把這種行為稱為智能轉換。
??在eval函數中,在你檢查過變量e是否為Num類型之后,編譯器就把它當成Num類型的變量解釋。于是你不需要進行顯式轉換就可以像這樣訪問Num的屬性value:e.value。Sum的屬性left和right也是這樣:在對應的上下文中,只需要寫e.left和e.right。在IDE中,這種智能轉換過的值會用不同的背景顏色著重表示,這樣更容易發現這個值的類型是事先檢查過的,如下圖所示:
智能轉換只在變量經過is檢查之后不再發生變化的情況下有效,當你對一個類的屬性進行智能轉換的時候,就像這個例子中的一樣,這個屬性必須是一個val屬性,而且不能有自定義的訪問器。否則,每次對屬性的訪問是否都能返回同樣的值將無從驗證。使用as關鍵字來表示特定類型的顯式轉換。
接下來看看怎樣把eval函數重構成更符合Kotlin語言習慣的風格。
2.3.6 重構:用“when”代替“if”
Kotlin和Java中的if有什么不同,你已經看到了。本章開始的時候,你見過if表達式用在適用Java三元運算符的上下文:if(a>b)a else b和 a > b ? a : b效果一樣。Kotlin沒有三元運算符,因為if表達式有返回值,這一點和Java不同。這意味著你可以用表達式體語法重寫eval函數,去掉return語句和花括號,使用if表達式作為函數體。
interface Expr
class Num(var value:Int):Expr //簡單的值對象類,只有一個屬性value,實現了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum運算的實參可以是任何Expr:Num或者另外一個Sum
fun eval(e:Expr) : Int =
if(e is Num){
e.value
} else if(e is Sum){
eval(e.left) + eval(e.right)
}else{
throw IllegalArgumentException("Unknown expression")
}
如果if分支中只有一個表達式,花括號是可以省略的。如果if分支是一個代碼塊,代碼塊中的最后一個表達式會被作為結果返回。
??讓我們進一步打磨代碼,使用when來重寫它。
interface Expr
class Num(var value:Int):Expr //簡單的值對象類,只有一個屬性value,實現了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum運算的實參可以是任何Expr:Num或者另外一個Sum
fun eval(e:Expr) : Int =
when(e){
is Num ->
e.value
is Sum ->
eval(e.left) + eval(e.right)
else ->
throw IllegalArgumentException("Unknown expression")
}
when表達式并不僅限于檢查值是否相等,那是之前你看到的。而這里使用了另外一種when分支的形式,允許你檢查when實參值的類型。和上述if的例子一樣,類型檢查應用了一次智能轉換,所以不需要額外的轉換就可以訪問Num和Sum的成員。
??比較最后兩個Kotlin版本的eval函數,想一想你應該怎樣在自己的代碼中也使用when代替連串的if表達式。當分支邏輯太過復雜時,可以使用代碼塊作為分支體。我們來看看這種用法。
2.3.7 代碼塊作為“if”和“when”的分支
if和when都可以使用代碼塊作為分支體。這種情況下,代碼塊中的最后一個表達式就是結果。如果在例子函數中加入日志,可以在代碼塊中實現它并像之前一樣返回最后的值。
interface Expr
class Num(var value:Int):Expr //簡單的值對象類,只有一個屬性value,實現了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum運算的實參可以是任何Expr:Num或者另外一個Sum
fun main(args: Array<String>) {
println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}
fun evalWithLogging(e:Expr) : Int =
when(e){
is Num ->{
println("num:${e.value}")
e.value
}
is Sum ->{
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("Sum:$left + $right")
left + right
}
else -> throw IllegalArgumentException("Unknown expression")
}
輸出結果:
num:1
num:2
Sum:1 + 2
num:4
Sum:3 + 4
7
規則————“代碼中最后的表達式就是結果”,在所有使用代碼塊并期望得到一個結果的地方成立。你會在文章末尾看到,同樣的規則對try主體和catch子句也有效,而文章K05還會討論該規則在lambda表達式中的應用。但是在2.2中我們提到。這個規則對常規函數不成立。一個函數要么具有不是代碼塊的表達式函數體,要么具有包含顯式return語句的代碼塊函數體。
??現在熟悉了Kotlin從眾多選項中做出正確選擇的方式,是時候看看怎樣迭代事物了。
2.4 迭代事物:“while”循環和“for”語句
在本章討論的所有特性中,Kotlin的迭代應該是和Java最接近的。when循環和Java完全一樣,本節開頭會一筆帶過。for循環僅以唯一一種形式存在,和Java的for-each循環一致。其寫法for <item> in
<elements>和C#一樣。和Java一樣,循環最常見的應用就是迭代集合。我們也會探索它是怎樣覆蓋其他使用循環的場景的。
2.4.1 “while”循環
Kotlin有while循環和do-while循環,它們的語法和Java相應的循環沒有什么區別:
while(condition){
/*...*/
}//先判斷,后執行
do{
/*...*/
} while(condition)//先執行,后判斷
Kotlin并沒有給這些簡單的循環帶來任何新東西,所以不必停留。我們繼續討論for循環的各種用法。
2.4.2 迭代數字:區間和數列
正如我們剛剛提到的那樣,在Kotlin中沒有常規的Java for循環。在這種循環中,先初始化變量,在循環的每一步更新它的值,并在值滿足某個限制條件時退出循環。為了替代這種最常見的循環用法,Kotlin使用了區間的概念。
??區間本質上就是兩個值之間的間隔,這兩個值通常是數字:一個起始值,一個結束值。使用..運算符來表示區間:
var oneToTen = 1..10
注意Kotlin的區間是包含的或者閉合的,意味著第二個值始終是區間的一部分。
??你能用整數區間做的最基本的事情就是循環迭代其中所有的值。如果你能迭代區間中所有的值,這樣的區間被稱作數列。
??讓我們用整數迭代來玩Fizz-Buzz游戲。這是一種用來打發長途駕駛旅程的不錯方式,還能幫你回憶起被遺忘的除法技巧。游戲玩家輪流遞增計數,遇到能被3整數的數字就用單詞fizz代替,遇到能被5整除的數字則用單詞buzz代替。如果一個數字3和5的公倍數,你得說出“FizzBuzz”。
??下面的代碼打印出了游戲中1到100之間所有的數字的正確答案。注意你是怎樣用不帶參數的when表達式來檢查可能的條件的。
fun main(args: Array<String>) {
for(i in 1..100){
println(fizzBuzz(i))
}
}
fun fizzBuzz(i:Int) = when{
i % 15 == 0 ->"FizzBuzz"
i % 3 == 0 ->"Fizz"
i % 5 == 0 ->"Buzz"
else -> "$i"
}
假設一個小時的駕駛之后,你已經厭倦了這些規則,想把游戲變得復雜一點,那我們可以從100開始倒著技術并且只計偶數。
??現在你在迭代一個帶步長的數列,它允許跳過一些數字。步長也可以是負數,這種情況下數列是遞減而不是遞增的。在這個例子中,100 downTo 1是遞減的數列(步長為-1)。然后step把步長的絕對值變成了2,但方向保持不變(事實上,步長被設置成了-2)。
??如前所述,..語法始終創建的是包含結束值(..右邊的值)的區間。許多情況下,迭代不包括指定結束值的半閉合區間更方便。使用until函數可以創建這樣的區間。例如,循環for(x in 0 until size)雖然等同于for(x in 0..size-1),但是更清晰地表達了意圖。在之后的文章中,你會學習更多關于這些例子中downTo,step和until的語法。
??可以看到使用區間和數列是怎樣幫助你應付FizzBuzz游戲的進階規則的。現在讓我們看看其他使用for循環的例子。
2.4.3 迭代map
我們提到了使用for...in循環的最常見的場景迭代集合。這和Java中的用法一樣,所以我們不會講太多關于它的內容。讓我們來看看你可以怎樣迭代map。
??作為例子,我們看看這個打印字符二進制表示的小程序。你會把這些二進制表示保存在一個map中(僅說明之用)。下面的代碼創建了一個map,把某些字母的二進制填充進去,最后打印map的內容。
fun main(args: Array<String>) {
val binaryReps = TreeMap<Char,String>() //使用TreeMap讓鍵排序
for(c in 'A'..'F'){ //使用字符區間迭代從A到F之間的字符
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary //根據c把值存儲到map中
}
for((letter,binary) in binaryReps){//迭代map,把鍵和值賦給兩個變量
println("$letter = $binary")
}
}
..語法不僅可以創建數字區間,還可以創建字符區間。這里使用它迭代從A開始到F的所有字符,包括F。
??展示了for循環允許展開迭代中的集合的元素(在這個例子中,展開的是map的鍵值對集合)。把展開的結果存儲到了兩個獨立的變量中:letter是鍵,binary是值。稍后,你將學到更多的展開語法。
??上述代碼中使用了一個使用的小技巧,根據鍵來訪問和更新map的簡明語法。可以使用map[key]
讀取值,并使用map[key] = value設置它們,而不需要調用get和put。下面這段代碼
binaryReps[c] = binary//等價于Java版本代碼“binaryReps.put(c,binary);”
可以用這樣的展開語法在迭代集合的同時跟蹤當前項的下標。不需要創建一個單獨的變量來存儲下標并手動增加它:
val list = arrayListOf("10","11","1001")
for((index,element) in list.withIndex()){ //迭代集合時使用下標
println("$index:$element")
}
之后的文章我們會探索關于withIndex的內容。
??我能已經看過了如何使用關鍵字in來迭代區間或者集合,還可以用in來檢查區間或者集合是否包含了某個值。
2.4.4 使用“in”檢查集合和區間的成員
使用in運算來檢查一個值是否在區間中,或者它的逆運算,!in,來檢查這個值是否不在區間中。下面展示了如何使用in來檢查一個字符是否屬于一個字符區間。
//使用in來檢查一個字符是否屬于英文字母
fun isLetter(c:Char) = c in 'a'..'z'||c in 'A'..'Z' //方式1
fun isNotDigit(c:Char) = c !in '0'..'9' //方式2
這種檢查字符是否是英文字母的技巧看起來很簡單。在底層,沒有什么特殊處理:你依然會檢查字符的編碼是否位于第一個字母編碼和最后一個字母編碼之間的某個位置。但是這個邏輯被簡潔地隱藏到了標準庫中的區間類實現中:
c in 'a'..'z' //變換成'a'<=c&&c<='z'
in運算符和!in運算符也適用于when表達式。
fun recognize(c : Char) = when(c){
in '0'..'9' -> "c是數字字符"
in 'a'..'z','A'..'Z' -> "c是英文字符"
else -> "不知道c是啥字符,反正不是數字字符和英文字符"
}
區間也不僅限于字符。假如有一個支持實例比較操作的任意類(實現了java.lang.Comparable接口),就能創建這種類型的對象的區間。如果這樣的區間,并不能列舉出這個區間種的所有的對象。想想這種情況:例如,是否可以列舉出“Java”和“Kotlin”之間所有的字符串?答案是不能。但是仍然可以使用in運算符檢查一個其他的對象是否屬于這個區間:
println("Kotlin" in "Java".."Scala") //結果和"Java"<="Kotlin" && "Kotlin"<="Scala"一樣
注意,這里字符串是按照字母排序進行比較的,因為String就是這樣實現Comparable接口的。
??in檢查也同樣適用于集合:
println("Kotlin" in setOf("Java","Scala")) //因為Set集合不包含“Kotlin”,輸出false
在之后的文章種我們將會對我們自己的數據類型的區間和數列使用in檢查學習。下面來看看Kotlin種的異常。
2.5 Kot中的異常
Kotlin中的異常處理和Java以及其他語言的處理方式相似。一個函數可以正常結束,也可以出現錯誤的情況下拋出異常。方法的調用者能捕獲這個異常并處理它;如果沒有被處理,異常會沿著調用棧再次拋出。
??Kotlin中異常處理語句的基本形式和Java類似,拋出異常的方式也不例外:
if(percentage !in 0..100){
throw IllegalArgumentException(
"A percentage value must be between 0 and 100:$percentage")
}
和所有其他類一樣,不必使用new關鍵字來創建異常實例。
??和Java不同的是,Kotlin中throw結構是一個表達式,能作為另一個表達式的一部分使用:
val percentage =
if(number in 0..100)
number
else
throw IllegalArgumentException(
"A percentage value must be between 0 and 100:$number")//throw是一個表達式
在這個例子中,如果條件滿足,程序的行為是正確的,而percentage變量會用number初始化。否則,異常將會拋出,而變量也不會初始化。在之后的文章中,我們將會討論討論關于throw作為其他表達式的一部分的技術細節。
2.5.1 “try” “catch” 和 “finally”
和Java一樣,使用帶有catch和finally子句的try結構來處理異常。你會在下面這個代碼中看到這個結構,這個例子從給定的文件中讀取一行,嘗試把它解析成一個數字,返回這個數字;或者當這一行不是一個有效數字時返回null。
fun readNumber(reader:BufferedReader):Int?{
try{
val line = read.readLine()
return Integer.parseInt(line)
}catch(e : NumberFormatException){
return null
}finally{
reader.close()
}
}
和Java最大的區別就是throws子句沒有出現在代碼中:如果用Java來寫這個函數,你會顯式地在函數聲明后寫上throws IOException。你需要這樣做的原因是IOException是一個受檢異常。在Java中,這種異常必須顯式地處理。必須聲明你的函數能拋出的所有受檢異常。如果你調用另外一個函數,需要處理這個函數的受檢異常,或者聲明你的函數也能拋出這些異常。
??和其他許多現代的JVM語言一樣,Kotlin并不區分受檢異常和未受檢異常。不用指定函數拋出的異常,而且可以處理也可以不處理異常。這種設計是基于Java中使用異常的實踐做出的決定。經驗顯示這些Java規則常常導致許多毫無意義的重新拋出或者忽略異常的代碼,而且這些規則不能總是保護你免受可能發送的錯誤。
??在上述代碼中,NumberFormatException就不是受檢異常。因此,Java編譯器并不會強迫你捕獲它,在運行時很容易看到這個異常發生。與此同時,BufferedReader.close可能拋出需要處理的受檢異常IOExceptio。如果流關閉失敗,大多數程序都不會采取什么有意義的行動,所以捕獲來自close()的異常所需要的代碼就是冗余的樣板代碼。
2.5.2 “try”作為表達式
為了了解JavaK和otlin之間另外一個顯著的差異,我們修改一下這個例子。讓我們去掉finally部分(因為你已經看過它是怎樣工作的),并添加一些代碼,用來打印從文件中讀取的數字。
fun readerNumber(reader:BufferedReader){
val number = try{
Integer.parseInt(reader.readLine)//變成try表達式的值
}catch(e : NumberFormatException){
return
}
println(number)
}
Kotlin中的try關鍵字就像if和when一樣,引入了一個表達式,可以把它的值賦給一個變量。不同于if,你總是需要用花括號把語句主體括起來。和其他語句一樣,如果其主體包含多個表達式,那么整個try表達式的值就是最后一個表達式的值。
??這個例子將return語句放在catch代碼塊中,因此此函數的執行在catch代碼塊之后不會繼續。如果你想繼續執行,catch子句也需要一個值,它將是子句中最后一個表達式的值。下面展示了這是怎么回事。
fun readerNumber(reader:BufferedReader){
val number = try{
Integer.parseInt(reader.readLine)//沒有任何異常發生時使用這個值
}catch(e : NumberFormatException){
null //發生異常情況下使用null
}
println(number)
}
如果一個try代碼塊執行一切正常,代碼塊中最后一個表達式就是結果。如果捕獲到了一個異常,相應catch代碼塊中最后一個表達式就是結果。在上述代碼中,如果捕獲了NumberForMatException,結果值就是null。
2.6 小結
- (1)fun關鍵字用來聲明函數。val關鍵字和var關鍵字分別用來聲明只讀變量和可變變量。
- (2)字符串模板幫助你避免煩瑣的字符串連接。在變量名稱前面加上$前綴或者用${}包圍一個表達式,來把值注入到字符串中。
- (3)值對象類在Kotlin中以簡潔的方式表示。
- (4)熟悉的if現在是帶返回值的表達式。
- (5)when表達式類似于Java中的switch但功能更強大。
- (6)在檢查過變量具有某種類型之后不必顯示地轉換它的類型:編譯器使用智能轉換自動幫你完成
- (7)for,while和do-while循環于Java相似,但是for循環現在更加方便,特別是當你需要迭代Map的時候,又或者迭代集合需要下標的時候。
- (8)簡明的語法1..5會創建一個區間。區間和數列允許Kotlin在for循環中使用統一的語法和同一套抽象機制,并且還可以使用in運算符和!in運算符來檢查值是否屬于某個區間。
- (9)Kotlin中的異常處理和Java非常相似,除了Kotlin不要求你聲明函數可以拋出的異常。
第二章的內容到這里就結束了,謝謝大家的支持,我也學習,你們也學習了,如果大家喜歡這本《Kotlin實戰》中文版的內容,那就購買一本吧,筆者覺得是一本非常不錯的Kotlin語言學習書籍,你可以看看筆者總結的第二章,喜歡就購買一本吧!才70元而已,知識無價。