🔧 擴展函數
📖 什麼是擴展函數?
**擴展函數(Extension Function)**是一種不修改現有類別就能新增函數的功能。可以像原本就存在的方法一樣使用!
💡 基本用法
第一個擴展函數
// 為 String 新增函數!
fun String.isEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
fun main() {
val email = "hong@example.com"
println(email.isEmail()) // true
println("invalid".isEmail()) // false
}
擴展屬性
val String.firstChar: Char
get() = if (this.isNotEmpty()) this[0] else ' '
fun main() {
println("Hello".firstChar) // H
println("Kotlin".firstChar) // K
}
🎯 實戰範例
字串工具
// 電話號碼格式
fun String.toPhoneFormat(): String {
return if (this.length == 11) {
"${substring(0, 3)}-${substring(3, 7)}-${substring(7)}"
} else {
this
}
}
// 截斷字串
fun String.truncate(length: Int): String {
return if (this.length > length) {
"${substring(0, length)}..."
} else {
this
}
}
// 只提取數字
fun String.numbersOnly(): String {
return this.filter { it.isDigit() }
}
fun main() {
println("01012345678".toPhoneFormat()) // 010-1234-5678
val long = "This is a very long text"
println(long.truncate(10)) // This is a ...
println("價格:1,500元".numbersOnly()) // 1500
}
數字工具
// 判斷偶數
fun Int.isEven(): Boolean = this % 2 == 0
// 範圍檢查
fun Int.isBetween(min: Int, max: Int): Boolean {
return this in min..max
}
// 貨幣格式
fun Int.toCurrency(): String {
return "%,d元".format(this)
}
fun main() {
println(4.isEven()) // true
println(7.isEven()) // false
println(50.isBetween(0, 100)) // true
println(1500000.toCurrency()) // 1,500,000元
}
集合工具
// 安全的第二個元素
fun <T> List<T>.secondOrNull(): T? {
return if (this.size >= 2) this[1] else null
}
// 符合條件的第一個索引
fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
val index = this.indexOfFirst(predicate)
return if (index >= 0) index else null
}
// 將列表分成 n 個一組
fun <T> List<T>.chunked(size: Int): List<List<T>> {
return this.chunked(size)
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.secondOrNull()) // 2
val index = numbers.indexOfFirstOrNull { it > 3 }
println(index) // 3 (索引)
println(numbers.chunked(2)) // [[1, 2], [3, 4], [5]]
}
日期/時間(簡化版本)
data class SimpleDate(val year: Int, val month: Int, val day: Int)
fun SimpleDate.format(): String {
return "%04d-%02d-%02d".format(year, month, day)
}
fun SimpleDate.isWeekend(): Boolean {
// 簡單計算星期(Zeller's congruence)
val y = if (month < 3) year - 1 else year
val m = if (month < 3) month + 12 else month
val dayOfWeek = (day + (13 * (m + 1) / 5) + (y % 100) +
(y % 100) / 4 + (y / 100) / 4 - 2 * (y / 100)) % 7
return dayOfWeek == 0 || dayOfWeek == 6 // 週六/日
}
fun main() {
val date = SimpleDate(2024, 12, 25)
println(date.format()) // 2024-12-25
}
🔍 nullable 擴展
null 安全的擴展
// null 或空字串
fun String?.isNullOrEmpty(): Boolean {
return this == null || this.isEmpty()
}
// 回傳預設值
fun String?.orDefault(default: String): String {
return this ?: default
}
fun main() {
val text: String? = null
println(text.isNullOrEmpty()) // true
println(text.orDefault("預設值")) // 預設值
}
🎨 進階應用
泛型擴展
// 打亂集合
fun <T> List<T>.shuffled(): List<T> {
return this.shuffled()
}
// 條件轉換
fun <T, R> T.letIf(condition: Boolean, block: (T) -> R): R? {
return if (condition) block(this) else null
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.shuffled())
val result = "hello".letIf(true) { it.uppercase() }
println(result) // HELLO
}
中綴函數
// 中綴表示法
infix fun Int.multipliedBy(other: Int): Int {
return this * other
}
infix fun String.concat(other: String): String {
return "$this $other"
}
fun main() {
println(5 multipliedBy 3) // 15
val greeting = "Hello" concat "World"
println(greeting) // Hello World
}
🛠️ 實用擴展集合
驗證擴展
// 電子郵件驗證
fun String.isValidEmail(): Boolean {
val pattern = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+"
return this.matches(pattern.toRegex())
}
// 密碼強度
fun String.isStrongPassword(): Boolean {
return this.length >= 8 &&
this.any { it.isDigit() } &&
this.any { it.isUpperCase() } &&
this.any { it.isLowerCase() }
}
// URL 驗證
fun String.isValidUrl(): Boolean {
return this.startsWith("http://") || this.startsWith("https://")
}
fun main() {
println("hong@example.com".isValidEmail()) // true
println("Password123".isStrongPassword()) // true
println("https://kotlin.org".isValidUrl()) // true
}
轉換擴展
// 駝峰式 → 蛇形式
fun String.toSnakeCase(): String {
return this.replace(Regex("([a-z])([A-Z])")) {
"${it.groupValues[1]}_${it.groupValues[2]}"
}.lowercase()
}
// 蛇形式 → 駝峰式
fun String.toCamelCase(): String {
return this.split("_").mapIndexed { index, word ->
if (index == 0) word else word.capitalize()
}.joinToString("")
}
fun main() {
println("userName".toSnakeCase()) // user_name
println("user_name".toCamelCase()) // userName
}
🤔 常見問題
Q1. 擴展函數應該定義在哪裡?
A: 通常定義在獨立檔案中!
// StringExtensions.kt
fun String.isEmail(): Boolean {
return this.contains("@")
}
// NumberExtensions.kt
fun Int.isEven(): Boolean {
return this % 2 == 0
}
// 使用時需要 import
import com.example.extensions.*
Q2. 如果與現有方法同名會怎樣?
A: 現有方法優先!
class MyClass {
fun test() {
println("原始方法")
}
}
// 擴展函數(不會被呼叫!)
fun MyClass.test() {
println("擴展函數")
}
fun main() {
MyClass().test() // 原始方法
}
Q3. 可以存取 private 成員嗎?
A: 不可以!
class Person(private val age: Int)
// ❌ 無法存取 private
fun Person.getAge(): Int {
// return this.age // 錯誤!
return 0
}
🎬 總結
用擴展函數讓程式碼更便利!
核心重點:
✅ 不修改現有類別就能新增功能
✅ fun 類型.函數名稱() 形式
✅ nullable 類型也可擴展
✅ 無法存取 private 成員
✅ 可作為工具函數使用
下一步:在例外處理中學習如何安全地處理錯誤!