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 (參數不同)。