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

605 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### 1. Kotlin應用開發初體驗
- 安裝[IntelliJ IDEA](https://www.jetbrains.com/idea/)
- 在.kt檔案寫一個`main()`,旁邊會出現小箭頭,就可以直接執行。
![[Pasted image 20201225114228.png]]
- 註解
- `//`: 以兩個斜線(`/`)開頭就是註解,會被編譯器忽略
- `/* 這行文字是註解 */`: 這是另一種註解,被`/*``*/`包圍起來的區段都是註解,這種方可以跨行註解,但是注意,`/* */`裡面不可以再有另一個`/* */`方式的註解。
### 2. 變數、常數和類型
#### 定義一個「可變」變數
```Kotlin
var experiencePoints: Int = 5
--- --- -
^ ^ ^
| | |
| | Assign value
| Type
Keyword
```
使用`var`所定義的變數可以再度被改變,例如:
```
experiencePoints = 10
```
就會把`experiencePoints`的值變為10。
#### 定義一個「唯讀」變數
使用`val`所定義的變數不可以再被改變,例如,定義一個名叫`myLuckyNumber`的「唯讀」變數:
```Kotlin
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中只有「參照類型」也就是說基礎類別都是大寫像是`Int``String``Double``Boolean`...等等。
雖然說Compiler會有條件的把參照類型轉為基礎類型來增加效率但對於開發者來說大都是不需在意的。
### 3. 條件運算式
#### if/else
語法
```kotlin
if (<condition>) {
// code block if <condition> is true
} else {
// code block if <condition> is false
}
```
`<condition>``true`的時候,`if`區塊裡面的code就會被執行
##### 多重條件
當有多個condition的時候可以用`else if`來增加條件,如下:
```kotlin
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. 函數
#### 函數的結構
一個函數的結構如下
```kotlin
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也可以指定給變數。如以下例子
```kotlin
val get5 = {
val a = 2
val b = 3
a + b // 最後一行為返回值
}
// 呼叫lambda
val number = get5()
```
lambda類型是由lambda的輸入參數、輸出類型所定義的。
#### lambda的參數與返回值
方法1將參數類型與返回值定義在變數裡
```kotlin
val addResult: (a: Int, b: Int) -> Int = {
a + b
}
```
上面的例子定義了`addResult`這個lambda輸入參數有`a``b`兩個,兩個的類型都是`Int`,返回值也是`Int``{}`內則是實作。
方法二:將參數類型與返回值定義在變數裡,參數命名則在函數本體裡
```kotlin
val addResult: (Int, Int) -> Int = {a, b ->
a + b
}
```
上面的例子定義了`addResult`這個lambda輸入是兩個類型為Int的參數返回類型也是Int參數名稱`a``b`則定義在`{}`這也表示參數名稱可以由lamdba提供者隨意修改。
#### lambda的類型推斷
如同編譯器可以自東推斷變數的類型lambda的類型也可以自動推斷例如
```kotlin
val returnHello: () -> String = {
"Hello"
}
```
可以簡化成:
```kotlin
val returnHello = {
"Hello"
}
```
很顯然的這一個沒有輸入參數回傳值String的lambda。
對於有多個參數的lambda則需要清楚的把參數的名字與類型都寫出來例如
```kotlin
val sayHello = { name: String, age: Int
"Hello, I'm $name, $age years old."
}
```
`sayHello()`的推斷類型是輸入有兩個參數一個是型別為String的name另一個是型別為Int的age然後根據[[20201218 - Kotlin權威2.0#隱式返回]]最後一行是回傳值所以返回值是String型別。這寫法跟下面的寫法同意
```kotlin
val sayHello: (String, Int) -> String = { name, age
"Hello, I'm $name, $age years old."
}
```
#### `it`關鍵字
當lambda只有一個參數的時候可以用it來當參數的名字例如
```kotlin
val add5: (Int) -> Int = {
it + 5
}
```
使用it雖然方便但是對可讀性卻沒有比較好這點自己權衡使用。
#### 將lambda當作參數
lambda可以當作參數傳給函數只要在函數內定義好lambda的類型即可。例如我們可以設計一個函數接收不同「打招呼lamdba」來產生打招呼字串我們先定義3個打招呼lambda
1.
```kotlin
val sayHi = { name: String,
"Hi $name"
}
```
2.
```kotlin
val sayHello = { name: String,
"Hello $name"
}
```
3.
```kotlin
val sayGoodMornig = { name: String,
"Good mornig, $name"
}
```
這三個lambda都有同樣的型別型別都是 (String) -> String也就是輸入參數是一個String返回值也是String。
接下來,定義我們要使用的函數:
```kotlin
fun greet(name: String, greetFunc: (String) -> String): String {
return greetFunc(name)
}
```
然後我們就可以這樣用:
```kotlin
val greetString1 = greet("John", sayHi)
val greetString2 = greet("John", sayHello)
val greetString3 = greet("John", sayGoodMornig)
```
#### 將lambda當作參數的簡略語法
如果lambda參數是函數的最後一個參數那麼便可以使用簡略語法我們用上面的例子一步一步來看。例如我們直接將`sayHi` lambda寫在`greet()`裡面:
```kotlin
val greetString1 = greet("John", { name: String ->
"Hi $name"
})
```
因為lambda是最後一個所以可以將lambda移到外面來
```kotlin
val greetString1 = greet("John") { name: String ->
"Hi $name"
}
```
又因為這個lambda只有一個參數所以可以用`it`來簡化它,變成:
```kotlin
val greetString1 = greet("John") {
"Hi $it"
}
```
#### inline function
若想要避免lambda產生記憶體開銷就可以使用`inline`關鍵字,`inline`關鍵字會函數在使用的地方展開例如剛剛的sayHi例子我們將使用lambda的`greet()`函數加上inline變成
```kotlin
inline fun greet(name: String, greetFunc: (String) -> String): String {
return greetFunc(name)
}
```
那麼`greet()`就會在呼叫處直接展開,就好像:
```kotlin
{
val result = "Hi $name"
return result
}
```
#### 將函數當作參數
用fun定義的函數也可以像lambda一樣當作參數只是要在呼叫的時候在函數名前面加上`::`,例如:
```kotlin
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`來讓使用呼叫:
```kotlin
fun selectGreet(number: Int): (String) -> String {
when (number) {
1 -> sayHi,
2 -> sayHello,
3 -> sayGoodMornig
}
}
```
可以看到`selectGreet`的回傳值是`(String) -> String`,接下來我們可以這樣用:
```kotlin
val greetFunc = selectGreet(2)
val greetString = greetFunc("John") // greetString會是"Hi John"
```
#### lambda也是closure
若是在函數裡面回傳一個lambda的時候回傳的那個lambda在被呼叫的時候還是可以使用當初函數所在位置的變數這便是closure。例如
```kotlin
fun countGreet(): (String) -> String {
var count = 0
return { name ->
count = count + 1
"[$count] Hi $name"
}
}
```
當我們呼叫它的時候:
```kotlin
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"
```
雖然`count``countGreet()`內的區域變數,但是`greetFunc()`還是能繼續取用它。
能夠接受函數或是lambda當參數或是返回函數的函數又叫做**高階函數**。
### 6. Nullability
Kotlin預設是型別都不可以是null。如果有一個型別是`String`的變數把它設為null的話compiler就會報錯。
```kotlin
var name = "John"
name = null <-- Error!
```
如果一定要設為null那麼必須在宣告的時候`?`符號告訴compiler說這個變數必須是「可以null的」。
```kotlin
var name: String? = "John"
name = null <-- OK
```
`?`也可以用來判斷函數的回傳值例如有一個函數它會回傳一個字串或一個null
```kotlin
fun getString(number: Int): String? {
if (number > 90) {
return "good"
} else {
return null
}
}
```
那我們在呼叫這個函數之後,可以方便的用`?`來串接下一個步驟:
```kotlin
val status = getString(50)?.capitialize()
```
上面例子是我們在得到"good"字串後,呼叫`String.capitialize()`來把字串的第一個字元變成大寫。`?`符號可以幫我們判斷`getString()`回傳的是不是null如果不是就接著呼叫`capitialize()`如果是null`capitialize()`就不會被呼叫status將會是null。上面例子跟下面的程式是一樣的效果但是明顯簡短的多
```kotlin
val status = getString(50)
var statusCapital: String? = null
if (status) {
statusCapital = status.capitialize()
}
```
如果串接函數很多個的時候,更能看出效果:
```kotlin
val number = funcA()?.funcB()?.funcC()?.funcD()
```
#### `!!` operator
`!!`[not-null assertion](https://kotlinlang.org/docs/reference/null-safety.html#the--operator) 用來讓compiler忽略null的檢查例如
```kotlin
var name: String? = null
name.capitialize() <-- 會報錯
name!!.capitialize() <-- 不會報錯但是runtime會錯
```
#### `?:` Elvis operator
`?:` 就是「要是左邊為false就執行右邊」`?:`可以很方便的用來設定變數的預設值,例如前面舉過的例子:
```kotlin
val number = funcA()?.funcB()?.funcC()?.funcD()
```
要是`funcA()``funcB()``funcC()`中任何一個的回傳是null那麼number都會因為無法求值而被設為null我們可以用`?:`來給它一個預設值:
```kotlin
val number = funcA()?.funcB()?.funcC()?.funcD() ?: "Default value"
```
#### 異常Exception
`throw`來拋出一個異常,例如:
```kotlin
throw IllegalStateException("Oh! Oh!")
```
#### 自訂異常
可以用繼承來建立自己的異常:
```kotlin
class MyIllegalException(): IllegalStateException("I like new Exception")
```
#### 處理異常
`try/catch`來處理異常:
```kotlin
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` |