Kotlin Extension Functions 擴充函式 & Extension Properties 擴充屬性

以前在 Java,如果想要幫 String 類別加一個功能(例如 isEmail()),我們通常會寫一個 StringUtils 工具類別:

// Java Way
StringUtils.isEmail("test@example.com");

在 Kotlin 中,我們可以 直接擴充該類別,即便無法修改該類別的原始碼(例如 String 是 JDK 的類別)。

定義擴充函式

只須在函式名稱前加上 類別名稱. 即可。

fun String.isEmail(): Boolean {
    // 這裡的 this 代表呼叫這個函式的 String 物件
    return this.contains("@")
}

// 使用
val email = "test@example.com"
if (email.isEmail()) {
    println("Valid!")
}

看!使用起來就像是 String 本來就有 isEmail() 這個方法一樣自然。

擴充屬性 (Extension Properties)

除了函式,也可以擴充屬性。但請注意,擴充屬性沒有 Backing Field (因為類別結構沒變),所以無法儲存狀態,只能定義 get() (如果是 var 則加上 set())。

val String.lastChar: Char
    get() = this.get(length - 1)
    // field = ... // 錯誤!不能使用 field

var StringBuilder.firstChar: Char
    get() = get(0)
    set(value) {
        this.setCharAt(0, value)
    }

println("ABC".lastChar) // 'C'
擴充函式其實是 靜態解析 (Statically resolved) 的。 它並沒有真正修改類別的結構,只是編譯器幫你把它轉成類似靜態方法的呼叫。

可包含 Null 的擴充 (Nullable Receiver)

你甚至可以擴充「可能為 null」的型別!這讓 null check 寫起來更優雅。

fun Any?.toStringOrEmpty(): String {
    if (this == null) return ""
    return this.toString()
}

val s: String? = null
println(s.toStringOrEmpty()) // 不會崩潰,印出空字串

類別成員擴充 (Declaring Extensions as Members)

你可以在一個類別 (Host) 裡面,為另一個類別 (Target) 定義擴充函式。這在建立特定領域語言 (DSL) 時非常有用。

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }

    // 在 Host 裡面擴充 Connection 類別
    fun Connection.connect() {
        printHostname()   // 可以呼叫 Host 的方法 (Dispatch Receiver)
        println(" connected to $ip") // 可以呼叫 Connection 的方法 (Extension Receiver)
    }

    fun doAction(c: Connection) {
        c.connect() // 只能在 Host 類別內部呼叫
    }
}

class Connection(val ip: String)

這種擴充函式只能在該類別內部,或是範圍內使用。

伴生物件擴充 (Companion Object Extensions)

如果你想為某個類別新增「靜態 (Static)」風格的擴充函式,可以擴充它的 Companion Object。

class MyClass {
    companion object { }
}

fun MyClass.Companion.printHello() {
    println("Hello from Extension")
}

// 呼叫方式
MyClass.printHello()

重要規則:成員優先 (Members always win)

如果擴充函式的名稱跟類別原本的成員函式 (Member Function) 一模一樣(名稱跟參數都相同),編譯器永遠會執行成員函式,擴充函式會被忽略。

class Person {
    fun speak() = println("I am a person")
}

fun Person.speak() = println("I am an extension")

Person().speak() // 印出 "I am a person"
這是一個常見的陷阱,定義擴充函式時請確保不會與現有成員衝突,除非你是故意要 Overload (參數不同)。