Files
Obsidian-Main/01. 個人/讀書筆記/20201218 - Kotlin權威2.0.md

20 KiB
Raw Blame History

1. Kotlin應用開發初體驗

  • 安裝IntelliJ IDEA

  • 在.kt檔案寫一個main(),旁邊會出現小箭頭,就可以直接執行。 !Pasted image 20201225114228.png

  • 註解

    • //: 以兩個斜線(/)開頭就是註解,會被編譯器忽略
    • /* 這行文字是註解 */: 這是另一種註解,被/**/包圍起來的區段都是註解,這種方可以跨行註解,但是注意,/* */裡面不可以再有另一個/* */方式的註解。

2. 變數、常數和類型

定義一個「可變」變數

var experiencePoints: Int = 5
---                   ---   -
 ^                     ^    ^
 |                     |    |
 |                     |    Assign value
 |                     Type
 Keyword

使用var所定義的變數可以再度被改變,例如:

experiencePoints = 10

就會把experiencePoints的值變為10。

定義一個「唯讀」變數

使用val所定義的變數不可以再被改變,例如,定義一個名叫myLuckyNumber的「唯讀」變數:

val myLuckyNumber: Int = 7

myLuckyNumber = 10 <-- ERROR!

要用哪一種方式來定義?

應該優先使用val來定義變數,遇到變數需要改變的時候再來把val換成var。這總比有人寫出了bug不小心改動了變數而造成玲成錯誤來的好。明確的錯誤總是比較容易解決。

類型推斷

Kotlin是一個強型別的語言每一個變數都要有一個明確的「類型」。但Kotlin也有類型推斷的能力。例如

val myLuckyNumber: Int = 7

因為很明確的myLuckyNumber要指定為77是一個整數Int),所以Int可以忽略,如下:

val myLuckyNumber = 7

在IntellJ中把游標停在變數上按下Ctrl + Shift + P你會看到它推斷出來的類型: !Pasted image 20201225120335.png

常數

const val來定一一個常數(不會變的)

const val MAX_SCORE = 100

const val是編譯時就定義好的,不同於val是執行時期才設定的。

深入學習

Java中有兩種類型「參照類型」reference types與「基礎類型」primitive types 參照類型有對應的code大都是一個class。基礎類型則沒有由keyword表示。 Java的「參照類型」都是大寫開頭例如

Integer point = 5;

基礎類型則是

int point = 5;

但在Kotlin中只有「參照類型」也就是說基礎類別都是大寫像是IntStringDoubleBoolean...等等。 雖然說Compiler會有條件的把參照類型轉為基礎類型來增加效率但對於開發者來說大都是不需在意的。

3. 條件運算式

if/else

語法

if (<condition>) {
    // code block if <condition> is true
} else {
    // code block if <condition> is false
}

<condition>true的時候,if區塊裡面的code就會被執行

多重條件

當有多個condition的時候可以用else if來增加條件,如下:

if (<condition_1>) {
    // code block if <condition_1> is true
} else if (<condition_2>) {
    // code block if <condition_2> is true
} else if (<condition_3>) {
    // code block if <condition_3> is true
else {
    // code block if doesn't match any conditions.
}
比較運算子

Kotlin使用的比較運算子如下

  • <: 左側值是否「小於」右側值
  • <=: 左側值是否「小於等於」右側值
  • >: 左側值是否「大於」右側值
  • >=: 左側值是否「大於等於」右側值
  • ==: 左側值是否「等於」右側值
  • !=: 左側值是否「不等於」右側值
  • ===: 左側的reference是否「等於」右側的reference
  • !==: 左側的reference是否「不等於」右側的reference
邏輯運算子
  • &&: AND
  • ||: OR
  • !: NOT
條件運算式

if/else運算式可以直接指派給一個變數,區塊內的最後一行會被當成回傳值設定給變數,例如:

val color = if (type == "tree") {
    println("type is tree")
    "GREEN" // <- 最後一行會是return value
} else {
    println("type is not tree")
    "WHITE" // <- 最後一行會是return value
}

range

Kotlin使用..來代表一個範圍,例如1..5會等於1, 2, 3, 4, 5。 在if/else裡面可以用來代替>, <之類的邏輯運算,例如:

if (score in 90..100) {
    println("A")
} else if (score in 80..89) {
    println("B")
} else {
    println("C")
}

..必須左邊小於右邊,若是要由大到小必須使用downTo

3 downTo 1 // 3, 2, 1

另外,跟..類似的until,差異是until不包含右邊的值:

1 until 3 // 1, 2
1..3      // 1, 2, 3

上述操作也可以轉成list呼叫.toList()即可:

(1..3).toList()       // [1, 2, 3]
(3 downTo 1).toList() // [3, 2, 1]

when

when類似C語言的switch,但是when更加靈活。先看一個例子:

val comment = when (score) {
    100       -> "Excellent"
    in 90..99 -> "A"
    in 80..89 -> "B"
    else      -> "C"
}

when會將score與->左邊的值做比較,要是成立就執行->右邊的區塊,跟if/else的條件運算一樣執行區塊內的最後一行會被return並指派給變數

val score = 99
val name = "Bond"
val comment = when (score) {
    100 -> {
        val message = "$name, you're Excellent
        message // <- 最後一行會是return value
    }
    in 90..99 -> {
        val message = "$name, you got A"
        message // <- 最後一行會是return value
    }
    in 80..89 -> {
        val message = "$name, you got B"
        message // <- 最後一行會是return value
    }
    else      -> {
        val message = "$name, you got C"
        message // <- 最後一行會是return value
    }
}

String範本

$開頭可以將變數的值帶入字串之中,例如:

val score = 100
println("My score is $score") // -> 印出"My score is 100"

另外,若用${}Kotlin會先將{}區塊求值,如此一來就可以很方便地在字串內做一些簡單處理或運算:

val a = 5
val b = 6
val result = "Result = ${a + b}"

4. 函數

函數的結構

一個函數的結構如下

private fun functionName(arg1: String, arg2: Int): String {
    // function body
}
  • private是「可見性修飾符」若是在檔案中則這個function只有在檔案中是可見的。
  • fun functionName是「函數名稱宣告」宣告一個函數的開始其中functionName可以自己命名。
  • (arg1: String, arg2: Int): 參數。每一組參數由,隔開。開頭是參數的名稱,:後面是參數的類型。以此例來說有2組參數第一組的參數叫做arg1,類型是String。第二組的參數叫做arg2,類型是Int
  • { }裡面是是函數運算本體。

預設引數

參數可以有一個預設值。例如下例:

fun sayHello(name: String): String {
    return "Hello $name!"
}

一定要傳入一個名字,我們可以預設讓它接受一個空字串,讓他單純說聲"Hello"就好。

fun sayHello(name: String=""): String {
    return "Hello $name!"
}

如此一來,使用者可以直接呼叫sayHello()就可以得到字串了。

單運算式函數

對於單純只有一行的函數,我們可以簡化函數的寫法,把大括號省略掉。以上面sayHello()的例子來說,可以簡略如下:

fun sayHello(name: String=""): String = "Hello $name!"

Unit函數

對於沒有返回值的函數,其返回值不是void,而是Unit。這類函數叫做「Unit函數」。

具名函數引數

呼叫函數時,一定要按照函數所定義的參數順序來填寫,否則會出錯,假設有一個函數定義如下:

fun getDiscountPrice(price: float, discount: float): float {
    return price * (1.0 - discount)
}

價錢與折數的順序要是錯位就會造成錯誤,這時候,呼叫函數時明確寫出參數名字可以避免這個情形:

val newPrice = getDiscountPrice(price = 1000.0,
                                discount = 0.3)

一旦使用具名引數,順序不對也沒有關係,像下面這樣寫也是可以的:

val newPrice = getDiscountPrice(discount = 0.3,
                                price = 1000.0)

Nothing類型

Nothing表示不可能執行成功。Kotlin標準程式庫的TODO()可以給一個經典的用法:

public inline fun TODO(): Nothing = throw NotImplementedError()

我們可以把TODO()用在還沒完成的函數上,例如:

fun notOkFunc(arg1: Int): Int {
    TODO("Someone finsih this")
    println("I don't want to implemnt this...") // <- This line is unreachable
}

因為某種原因notOkFunc()沒有實作完成,它也沒有返回一個Int的結果。但因為TODO()返回Nothing的關係所以編譯器就忽略了這個檢查反正它會執行失敗。

奇怪的函數名

一般來說函數的名字並不可以有空白或是一些特殊符號但是Kotlin支援用「反引號」來定義有特殊名字的函數。例如

fun `** Click to login **`(): Int {
    ...
}

呼叫時就變成:

val loginResult = `** Click to login **`()

但支援這種語法的主要原因是為了可以呼叫Java的API例如Java有一個叫做is()的函數,但是is是Kotlin的保留字用來檢查類型所以在Kotlin裡面要呼叫Java的is()就必須使用這個方法,例如:

fun callJavaIsInKotlin() {
    `is`()  // <- Invokes is() function from Java
}

5. 匿名函數與函數類型

匿名函數

{}裡面,沒有名字的就是匿名函數,定義一個簡單的匿名函數:

{
    println("Hello")
}

呼叫這個匿名函數:

{
    println("Hello")
}()

其實就跟呼叫一般函數一樣,只是()之前是一個函數本體,而不是函數名稱。 匿名函數在Kotlin叫做lambda以後都用lambda來稱呼匿名函數。

隱式返回

lambda預設會返回「最後一行」而且不能呼叫return。這是因為編譯器不知道返回資料是來自於lambda或是lambda的呼叫者。 下面這個無聊的lambda會返回一個字串

{
    "Hello"
}

lambda類型

lambda本身是一個類型type所以lambda也可以指定給變數。如以下例子

val get5 = {
    val a = 2
    val b = 3
    a + b // 最後一行為返回值
}

// 呼叫lambda
val number = get5()

lambda類型是由lambda的輸入參數、輸出類型所定義的。

lambda的參數與返回值

方法1將參數類型與返回值定義在變數裡

val addResult: (a: Int, b: Int) -> Int = {
    a + b
}

上面的例子定義了addResult這個lambda輸入參數有ab兩個,兩個的類型都是Int,返回值也是Int{}內則是實作。

方法二:將參數類型與返回值定義在變數裡,參數命名則在函數本體裡

val addResult: (Int, Int) -> Int = {a, b ->
    a + b
}

上面的例子定義了addResult這個lambda輸入是兩個類型為Int的參數返回類型也是Int參數名稱ab則定義在{}這也表示參數名稱可以由lamdba提供者隨意修改。

lambda的類型推斷

如同編譯器可以自東推斷變數的類型lambda的類型也可以自動推斷例如

val returnHello: () -> String = {
    "Hello"
}

可以簡化成:

val returnHello = {
    "Hello"
}

很顯然的這一個沒有輸入參數回傳值String的lambda。 對於有多個參數的lambda則需要清楚的把參數的名字與類型都寫出來例如

val sayHello = { name: String, age: Int
    "Hello, I'm $name, $age years old."
}

sayHello()的推斷類型是輸入有兩個參數一個是型別為String的name另一個是型別為Int的age然後根據20201218 - Kotlin權威2.0#隱式返回最後一行是回傳值所以返回值是String型別。這寫法跟下面的寫法同意

val sayHello: (String, Int) -> String = { name, age
    "Hello, I'm $name, $age years old."
}

it關鍵字

當lambda只有一個參數的時候可以用it來當參數的名字例如

val add5: (Int) -> Int = {
    it + 5
}

使用it雖然方便但是對可讀性卻沒有比較好這點自己權衡使用。

將lambda當作參數

lambda可以當作參數傳給函數只要在函數內定義好lambda的類型即可。例如我們可以設計一個函數接收不同「打招呼lamdba」來產生打招呼字串我們先定義3個打招呼lambda 1.

val sayHi = { name: String,
    "Hi $name"
}
val sayHello = { name: String,
    "Hello $name"
}
val sayGoodMornig = { name: String,
    "Good mornig, $name"
}

這三個lambda都有同樣的型別型別都是 (String) -> String也就是輸入參數是一個String返回值也是String。 接下來,定義我們要使用的函數:

fun greet(name: String, greetFunc: (String) -> String): String {
    return greetFunc(name)
}

然後我們就可以這樣用:

val greetString1 = greet("John", sayHi)
val greetString2 = greet("John", sayHello)
val greetString3 = greet("John", sayGoodMornig)

將lambda當作參數的簡略語法

如果lambda參數是函數的最後一個參數那麼便可以使用簡略語法我們用上面的例子一步一步來看。例如我們直接將sayHi lambda寫在greet()裡面:

val greetString1 = greet("John", { name: String ->
    "Hi $name"
})

因為lambda是最後一個所以可以將lambda移到外面來

val greetString1 = greet("John") { name: String ->
    "Hi $name"
}

又因為這個lambda只有一個參數所以可以用it來簡化它,變成:

val greetString1 = greet("John") { 
    "Hi $it"
}

inline function

若想要避免lambda產生記憶體開銷就可以使用inline關鍵字,inline關鍵字會函數在使用的地方展開例如剛剛的sayHi例子我們將使用lambda的greet()函數加上inline變成

inline fun greet(name: String, greetFunc: (String) -> String): String {
    return greetFunc(name)
}

那麼greet()就會在呼叫處直接展開,就好像:

{
    val result = "Hi $name"
    return result    
}

將函數當作參數

用fun定義的函數也可以像lambda一樣當作參數只是要在呼叫的時候在函數名前面加上::,例如:

fun sayHi(name: String): String {
    return "Hi $name"
}

fun greet(name, greetFunc: (String) -> String): String {
    val greetString = greetFunc(name)
    return greetString
}

// 呼叫
greet("John", ::sayHi)

在函數裡返回一個函數

若是將函數的返回值定義為相對應的函數類型即可以返回函數例如我們可以設計一個函數這個函數接受1、2、3三種數字當使用者輸入1的時候我們返回sayHi來讓使用呼叫當使用者輸入2的時候我們返回sayHelllo來讓使用呼叫當使用者輸入2的時候我們返回sayGoodMornig來讓使用呼叫:

fun selectGreet(number: Int): (String) -> String {
    when (number) {
    1 -> sayHi,
    2 -> sayHello,
    3 -> sayGoodMornig
    }
}

可以看到selectGreet的回傳值是(String) -> String,接下來我們可以這樣用:

val greetFunc = selectGreet(2)
val greetString = greetFunc("John") // greetString會是"Hi John"

lambda也是closure

若是在函數裡面回傳一個lambda的時候回傳的那個lambda在被呼叫的時候還是可以使用當初函數所在位置的變數這便是closure。例如

fun countGreet(): (String) -> String {
    var count = 0
    
    return { name -> 
        count = count + 1
        "[$count] Hi $name"
    }
}

當我們呼叫它的時候:

val greetFunc = countGreet()
val countString1 = greetFunc("John") // countString會等於"[1] Hi Jhon"
val countString2 = greetFunc("John") // countString會等於"[2] Hi Jhon"
val countString3 = greetFunc("John") // countString會等於"[3] Hi Jhon"

雖然countcountGreet()內的區域變數,但是greetFunc()還是能繼續取用它。 能夠接受函數或是lambda當參數或是返回函數的函數又叫做高階函數

6. Nullability

Kotlin預設是型別都不可以是null。如果有一個型別是String的變數把它設為null的話compiler就會報錯。

var name = "John"
name = null <-- Error!

如果一定要設為null那麼必須在宣告的時候?符號告訴compiler說這個變數必須是「可以null的」。

var name: String? = "John"
name = null <-- OK

?也可以用來判斷函數的回傳值例如有一個函數它會回傳一個字串或一個null

fun getString(number: Int): String? {
    if (number > 90) {
        return "good"
    } else {
        return null
    }
}

那我們在呼叫這個函數之後,可以方便的用?來串接下一個步驟:

val status = getString(50)?.capitialize()

上面例子是我們在得到"good"字串後,呼叫String.capitialize()來把字串的第一個字元變成大寫。?符號可以幫我們判斷getString()回傳的是不是null如果不是就接著呼叫capitialize()如果是nullcapitialize()就不會被呼叫status將會是null。上面例子跟下面的程式是一樣的效果但是明顯簡短的多

val status = getString(50)
var statusCapital: String? = null
if (status) {
    statusCapital = status.capitialize()
}

如果串接函數很多個的時候,更能看出效果:

val number = funcA()?.funcB()?.funcC()?.funcD()

!! operator

!!not-null assertion 用來讓compiler忽略null的檢查例如

var name: String? = null
name.capitialize()   <-- 會報錯
name!!.capitialize() <-- 不會報錯但是runtime會錯

?: Elvis operator

?: 就是「要是左邊為false就執行右邊」?:可以很方便的用來設定變數的預設值,例如前面舉過的例子:

val number = funcA()?.funcB()?.funcC()?.funcD()

要是funcA()funcB()funcC()中任何一個的回傳是null那麼number都會因為無法求值而被設為null我們可以用?:來給它一個預設值:

val number = funcA()?.funcB()?.funcC()?.funcD() ?: "Default value"

異常Exception

throw來拋出一個異常,例如:

throw IllegalStateException("Oh! Oh!")

自訂異常

可以用繼承來建立自己的異常:

class MyIllegalException(): IllegalStateException("I like new Exception")

處理異常

try/catch來處理異常:

var name: String? = null

name = somefunctionCall()

try: {
    val newName = name.capitialize()
}
catch (e: Exception) {
    println(e)
}

先決條件

類似C++中的assert()在符合判斷的條件下發出Exception。Kotlin內建5個先決條件函數

Funtion Description
checkNotNull() checkNotNull(condition, String)如果condition是null,就發出IllegalStateException
require() require(condition, String)如果condition是false,就發出IllegalStateException
requireNotNull() requireNotNull(condition, String)如果condition是null,就發出IllegalStateException
error error(condition, String)如果condition是null,就發出IllegalStateException
assert assert(condition, String)如果condition是false,就發出AssertError