Kotlin 函式 (Functions)
函式是程式邏輯的積木。
在 Kotlin 中,使用 fun 關鍵字來定義函式。
基本語法
fun sum(a: Int, b: Int): Int {
return a + b
}
- 參數必須定義型別 (
name: Type)。 - 參數在函式內是唯讀的 (
val),無法重新賦值。 - 回傳型別寫在括號後面 (
: ReturnType)。
// 呼叫方式
val result = sum(10, 20)
println(result) // 30
具名參數 (Named Arguments)
呼叫函式時,可以明確指定參數名稱。這對於有多個參數(尤其是部分帶有預設值)的函式非常有用,可以提高程式碼的可讀性。
假設我們有一個函式,有部分參數設有預設值:
fun createUser(
name: String,
role: String = "User",
notify: Boolean = true
) { ... }
我們可以使用以下幾種方式呼叫:
標準呼叫 (不使用參數名稱): 必須依照參數定義的順序傳值,且若要使用預設值只能從「最後面」開始省略。
createUser("Miko", "Admin", false) // 填滿所有參數 createUser("Miko") // 省略後面兩個預設參數使用具名參數: 指定參數名稱,順序可以隨意調換。
createUser( notify = false, name = "Miko", role = "Admin" )混合使用 (跳過中間的參數): 如果您想省略中間的某個參數(使用預設值),但在它之後的參數就必須使用具名參數。
// role 使用預設值 "User" // 因為跳過了 role,所以後面的 notify 必須指定名稱 createUser("Miko", notify = false)
函式重載 (Function Overloading)
就像 Java 或 C++ 一樣,你可以定義多個同名但參數列表不同 (參數型別或數量不同) 的函式。
fun read(b: ByteArray) { ... }
fun read(b: ByteArray, off: Int, len: Int) { ... }
儘管如此,在 Kotlin 中我們更推薦使用 預設參數 來取代大部分的重載需求。
預設參數 (Default Arguments)
Kotlin 允許你為參數設定預設值,這樣就可以減少大量的 Overloading (重載) 函式。
fun greeting(name: String, message: String = "Hello") {
println("$message, $name")
}
greeting("Miko") // 印出: Hello, Miko
greeting("Miko", "Hi") // 印出: Hi, Miko
Java 互通性 (@JvmOverloads)
預設參數 是 Kotlin 的功能,Java 原生並不支援。 如果你在 Kotlin 定義了一個帶有預設參數的函式,在 Java 中呼叫時,預設會視為「只有一個完整參數列表的方法」,你必須傳入所有參數,無法省略。
例如:
// Kotlin
fun greet(name: String = "World") { ... }
// Java
greet("Miko"); // OK
greet(); // Error! Java 找不到無參數的 greet() 方法
為了解決這個問題,可以使用 @JvmOverloads 註解。
編譯器會自動幫你產生多個「重載 (Overload)」的 Java 方法,每個重載方法會自動填入缺少的預設值並呼叫主方法。
@JvmOverloads
fun greet(name: String = "World") { ... }
加上註解後,編譯器會產生以下兩個 Java 方法供呼叫:
greet(String name)greet()(內部實作會自動呼叫greet("World"))
單一表達式函式 (Single-expression functions)
如果你的函式只有一行 return,可以簡化成這樣:
fun sum(a: Int, b: Int) = a + b
甚至連回傳型別 Int 都可以省略(自動推斷)。
無回傳值 (Unit)
如果函式不回傳任何東西(類似 Java 的 void),其實它是回傳 Unit 物件。
Unit 可以省略不寫。
fun printHello(name: String): Unit {
println("Hello $name")
}
// 等同於
fun printHello(name: String) {
println("Hello $name")
}
可變參數 (varargs)
如果你不確定參數有幾個(例如 Arrays.asList()),可以使用 vararg。
fun printAll(vararg messages: String) {
for (m in messages) println(m)
}
printAll("Hello", "World", "Kotlin")
// 如果你已經有一個 Array,想要傳進去,需要使用 Spread Operator (*)
val list = arrayOf("A", "B", "C")
printAll(*list)
中綴表示法 (Infix Notation)
讓程式碼讀起來像英文句子。條件:
- 必須是成員函式或擴充函式。
- 只有一個參數。
- 加上
infix關鍵字。
infix fun Int.plus(x: Int): Int {
return this + x
}
// 呼叫方式
val x = 1 plus 2 // 等同於 1.plus(2)
mapOf 使用的 to 其實就是一個 infix function (A to B)。
區域函式 (Local Functions)
Kotlin 支援在函式內部定義另一個函式。這對於封裝「只有這個函式會用到」的重複邏輯非常有幫助,而且區域函式可以直接存取外部函式的變數。
fun saveUser(user: User) {
// 定義區域函式,驗證邏輯不需暴露給外部
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("User $fieldName cannot be empty")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// save to database...
}
內聯函式 (Inline Functions)
使用高階函式 (High-order functions) 會帶來一些執行時期的效能開銷:每個函式都是一個物件,且會捕捉閉包 (Closure)。這些記憶體分配和虛擬呼叫 (Virtual calls) 會增加運作負擔。
使用 inline 修飾詞可以消除這些開銷。編譯器會在編譯時期,將函式的程式碼複製到呼叫處,而不是進行一般的函式呼叫。
inline fun <T> measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block()
println("Time: ${System.currentTimeMillis() - start} ms")
return result
}
// 呼叫處
measureTime {
// 這裡的程式碼在編譯後,會直接被複製到呼叫的位置
// 不會產生額外的 Function 物件
println("Do something...")
}
noinline
如果一個 inline 函式有多個 Lambda 參數,但你只想讓其中幾個被 inline,其他的想保留原本的函式物件形式(例如要將其傳給其他非 inline 函式,或儲存起來),可以使用 noinline 修飾詞。
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
inlined() // 會被 inline
notInlined() // 不會被 inline,保留為 Function 物件
}
crossinline
在 inline 函式傳入的 Lambda 中,預設允許使用 非局部返回 (Non-local returns)(即直接使用 return 來結束外部函式)。但如果這個 Lambda 是在 nested function 或 object 等其他執行空間(context)中被呼叫,就不能允許直接 return 外部函式。
什麼是非局部返回 (Non-local returns)?
簡單來說,非局部返回就是從一個 lambda 運算式內部,直接跳出包含該 lambda 運算式的函數本身。在 Kotlin 中,當你使用 return 關鍵字時,它通常會執行以下兩種行為:
- 局部返回 (Local Return):如果 return 在一個具名函數或匿名函數中使用,它會將控制權返回給這個函數的調用者(這是正常的函數行為)。
- 非局部返回 (Non-local Return):如果 return 在一個被內聯 (inlined) 函數調用的 lambda 運算式中使用,它會將控制權跳出到包含該內聯函數的函數。這就像是 lambda 體內的 return 實際上是包含它的外部函數的 return。
這時就需要加上 crossinline,它會保留 inline 的特性,但禁止在 Lambda 中使用 return(仍然可以使用 return@label)。
inline fun executeLater(crossinline body: () -> Unit) {
// 模擬在另一個 Context 執行,例如 Thread 或 Runnable
val runnable = object : Runnable {
override fun run() {
body() // 這裡呼叫了 body
}
}
runnable.run()
}
fun main() {
executeLater {
println("Hello")
// return // 錯誤!crossinline 禁止非局部返回
}
}
使用標籤(labels)進行局部返回
fun lookForSevenWithLabel(numbers: List<Int>) {
numbers.forEach myLabel@{ // 給 lambda 運算式命名一個標籤:myLabel
if (it == 7) {
println("找到 7 了,準備跳過")
return@myLabel // <--- 局部返回
// 退出 myLabel lambda 體,程式碼繼續執行 forEach 的下一個元素
}
println("正在處理 $it")
}
// forEach 循環完成後,程式碼會執行到這裡
println("列表遍歷完成")
}
fun main() {
val list = listOf(1, 2, 7, 4, 5)
lookForSevenWithLabel(list)
/* 輸出:
* 正在處理 1
* 正在處理 2
* 找到 7 了,準備跳過
* 正在處理 4
* 正在處理 5
* 列表遍歷完成
*/
}
使用匿名函數(Anonymous Function)進行局部返回
匿名函數的行為與普通函數相同,只允許局部返回。
fun lookForSevenWithAnonymous(numbers: List<Int>) {
// 使用匿名函數而不是 lambda
numbers.forEach(fun(it: Int) {
if (it == 7) {
println("找到 7 了,準備跳過")
return // <--- 局部返回
// 退出匿名函數本身,程式碼繼續執行 forEach 的下一個元素
}
println("正在處理 $it")
}) // 注意:這裡使用了圓括號來調動匿名函數
println("列表遍歷完成")
}
什麼是匿名函數(Anonymous Functions)?
匿名函數是與 Lambda 表達式非常相似的一種函數類型,但它們提供了一種更傳統、更靈活的方式來定義函數字面值(function literals)。
匿名函數的主要特徵是:
- 沒有名稱:因此被稱為「匿名」。
- 可以直接作為表達式使用:例如作為另一個函數的參數或賦值給一個變數,通常用於需要傳入函數的場景,例如
forEach、map、filter等高階函式。
匿名函數的語法:
fun (parameter1: Type1, parameter2: Type2): ReturnType {
// 函數主體 (Function body)
// ...
return result
}
範例:
val sum: (Int, Int) -> Int = fun(a: Int, b: Int): Int {
return a + b
}
val result = sum(5, 3) // result 是 8
注意: 儘管匿名函數的返回值型別通常可以從上下文推斷出來,但在函數參數列表之後明確指定返回型別 (: Int) 仍然是最佳實踐。
具體化型別參數 (Reified Type Parameters)
通常在 JVM 上,泛型會在編譯後被擦除 (Type Erasure),執行時期無法知道泛型的確切型別 T。例如你不能寫 T::class 或 obj is T。
但如果是 inline 函式,因為程式碼是直接複製到呼叫處,編譯器其實知道當下的型別是什麼。只要加上 reified 修飾詞,就可以在執行時期存取泛型型別。
這在 Gson 解析或 startActivity 等場景非常實用。
// 使用 reified 讓泛型與 T::class 可用
inline fun <reified T> printType() {
println(T::class.simpleName)
}
fun main() {
printType<String>() // 印出 String
printType<Int>() // 印出 Int
}
範例:簡化 Android 的 startActivity
// 一般寫法 (需要傳 class)
// startActivity(context, MainActivity::class.java)
// 使用 reified 優化
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
// 呼叫方式變得超簡潔
// context.startActivity<MainActivity>()