20 KiB
1. Kotlin應用開發初體驗
-
註解
//: 以兩個斜線(/)開頭就是註解,會被編譯器忽略/* 這行文字是註解 */: 這是另一種註解,被/*與*/包圍起來的區段都是註解,這種方可以跨行註解,但是注意,/* */裡面不可以再有另一個/* */方式的註解。
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要指定為7,7是一個整數(Int),所以Int可以忽略,如下:
val myLuckyNumber = 7
在IntellJ中,把游標停在變數上,按下Ctrl + Shift + P你會看到它推斷出來的類型:
!
常數
用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
語法
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,輸入參數有a、b兩個,兩個的類型都是Int,返回值也是Int。{}內則是實作。
方法二:將參數類型與返回值定義在變數裡,參數命名則在函數本體裡
val addResult: (Int, Int) -> Int = {a, b ->
a + b
}
上面的例子定義了addResult這個lambda,輸入是兩個類型為Int的參數,返回類型也是Int,參數名稱a、b則定義在{},這也表示參數名稱可以由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"
雖然count是countGreet()內的區域變數,但是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(),如果是null,capitialize()就不會被呼叫,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 |
