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
) { ... }

我們可以使用以下幾種方式呼叫:

  1. 標準呼叫 (不使用參數名稱): 必須依照參數定義的順序傳值,且若要使用預設值只能從「最後面」開始省略。

    createUser("Miko", "Admin", false) // 填滿所有參數
    createUser("Miko")                 // 省略後面兩個預設參數
    
  2. 使用具名參數: 指定參數名稱,順序可以隨意調換。

    createUser(
        notify = false,
        name = "Miko",
        role = "Admin"
    )
    
  3. 混合使用 (跳過中間的參數): 如果您想省略中間的某個參數(使用預設值),但在它之後的參數就必須使用具名參數。

    // 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 方法供呼叫:

  1. greet(String name)
  2. 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)

讓程式碼讀起來像英文句子。條件:

  1. 必須是成員函式或擴充函式。
  2. 只有一個參數。
  3. 加上 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 關鍵字時,它通常會執行以下兩種行為:

  1. 局部返回 (Local Return):如果 return 在一個具名函數或匿名函數中使用,它會將控制權返回給這個函數的調用者(這是正常的函數行為)。
  2. 非局部返回 (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)。

匿名函數的主要特徵是:

  • 沒有名稱:因此被稱為「匿名」。
  • 可以直接作為表達式使用:例如作為另一個函數的參數或賦值給一個變數,通常用於需要傳入函數的場景,例如 forEachmapfilter 等高階函式。

匿名函數的語法:

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::classobj 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>()