在掌握了 Kotlin 的基礎語法后,深入學習其進階特性將幫助你編寫更簡潔、高效且易維護的代碼。本課將詳細講解 Kotlin 中幾個重要的進階特性,包括擴展函數與屬性、委托、協(xié)程、枚舉類和注解。
1. 擴展函數與擴展屬性
Kotlin 允許我們在不修改原有類代碼的情況下,為其添加新的函數和屬性,這就是擴展(Extensions)特性。
1.1 擴展函數的定義
擴展函數的定義格式如下:
fun 接收者類型.函數名(參數列表): 返回值類型 {
// 函數體
}
其中,"接收者類型" 是我們要擴展的類,在函數體內可以使用 this 關鍵字引用接收者對象。
1.2 擴展函數的應用
擴展函數特別適合為系統(tǒng)類添加自定義方法,例如為 String 類添加一個判斷是否為郵箱的方法:
// 為 String 類添加擴展函數
fun String.isEmail(): Boolean {
val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".toRegex()
return matches(emailRegex)
}
// 使用擴展函數
fun main() {
val email = "test@example.com"
println(email.isEmail()) // 輸出: true
val notEmail = "test.example.com"
println(notEmail.isEmail()) // 輸出: false
}
擴展函數并不會真正修改目標類,它只是在編譯期為目標類添加了一個可調用的函數,在字節(jié)碼層面,它會被編譯為一個接受目標類型作為參數的靜態(tài)函數。
1.3 擴展屬性的定義與使用
除了函數,我們還可以為類添加擴展屬性:
// 為 String 類添加擴展屬性
val String.firstChar: Char?
get() = if (isEmpty()) null else this[0]
// 使用擴展屬性
fun main() {
val str = "Kotlin"
println(str.firstChar) // 輸出: K
val emptyStr = ""
println(emptyStr.firstChar) // 輸出: null
}
擴展屬性不能有初始化器,必須通過 getter/setter 來定義,因為它沒有實際的字段存儲。
2. 委托
委托(Delegation)是一種設計模式,它允許將一個類的部分功能委托給另一個類。Kotlin 原生支持委托模式,通過 by 關鍵字可以輕松實現。
2.1 委托模式的概念
委托模式的核心思想是:一個對象將部分職責委托給另一個對象來完成,從而實現對象之間的協(xié)作。這種模式可以替代繼承,實現更靈活的代碼復用。
2.2 類委托
類委托通過 by 關鍵字實現,允許一個類將接口的實現委托給另一個對象:
// 定義接口
interface Printer {
fun print(text: String)
}
// 實現接口的委托類
class ConsolePrinter : Printer {
override fun print(text: String) {
println("打印: $text")
}
}
// 使用委托的類
class DocumentPrinter(printer: Printer) : Printer by printer {
// 可以添加額外的方法或重寫委托的方法
fun printDocument(title: String, content: String) {
print("標題: $title")
print("內容: $content")
}
}
fun main() {
val consolePrinter = ConsolePrinter()
val docPrinter = DocumentPrinter(consolePrinter)
docPrinter.print("直接打印文本") // 委托給 ConsolePrinter 實現
docPrinter.printDocument(" Kotlin 委托", "類委托示例")
}
在這個例子中,DocumentPrinter 類通過 by printer 語法將 Printer 接口的實現委托給了 printer 對象。
2.3 屬性委托
屬性委托允許將屬性的 getter/setter 邏輯委托給一個專門的對象。Kotlin 標準庫提供了一些常用的委托實現:
2.3.1 by lazy(延遲初始化)
lazy 委托用于實現屬性的延遲初始化,屬性值會在第一次訪問時計算:
val expensiveResource: String by lazy {
println("初始化資源...")
"這是一個昂貴的資源"
}
fun main() {
println("開始")
println(expensiveResource) // 首次訪問,觸發(fā)初始化
println(expensiveResource) // 直接使用已初始化的值
}
輸出結果:
開始
初始化資源...
這是一個昂貴的資源
這是一個昂貴的資源
2.3.2 Delegates.observable(可觀察屬性)
Delegates.observable 可以監(jiān)聽屬性值的變化:
import kotlin.properties.Delegates
var username: String by Delegates.observable("默認值") {
property, oldValue, newValue ->
println("${property.name} 從 $oldValue 變?yōu)?$newValue")
}
fun main() {
username = "Alice"
username = "Bob"
}
輸出結果:
username 從 默認值 變?yōu)?Alice
username 從 Alice 變?yōu)?Bob
2.3.3 自定義委托
我們也可以實現自己的屬性委托,只需要實現 ReadOnlyProperty(只讀屬性)或 ReadWriteProperty(可讀寫屬性)接口:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// 自定義委托:確保值在 0-100 之間
class RangeValidator(private val min: Int, private val max: Int) :
ReadWriteProperty<Any?, Int> {
private var value: Int = min
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
if (value in min..max) {
this.value = value
} else {
throw IllegalArgumentException("值必須在 $min 到 $max 之間")
}
}
}
// 使用自定義委托
class Student {
var score: Int by RangeValidator(0, 100)
}
fun main() {
val student = Student()
student.score = 90 // 有效
println(student.score) // 輸出: 90
try {
student.score = 150 // 無效,會拋出異常
} catch (e: IllegalArgumentException) {
println(e.message) // 輸出: 值必須在 0 到 100 之間
}
}
3. 協(xié)程
協(xié)程(Coroutines)是 Kotlin 中處理異步編程的重要特性,它可以看作是輕量級的線程,能夠在單個線程內實現多個任務的切換執(zhí)行。
3.1 協(xié)程的概念
協(xié)程與線程的主要區(qū)別在于:
- 線程是操作系統(tǒng)級別的資源,創(chuàng)建和切換成本較高
- 協(xié)程是用戶態(tài)的,由 Kotlin 運行時管理,創(chuàng)建和切換成本極低
- 一個線程可以運行多個協(xié)程
協(xié)程的核心優(yōu)勢是能夠以同步的代碼風格編寫異步操作,避免了回調地獄(Callback Hell)。
3.2 協(xié)程的基礎使用
要使用協(xié)程,需要添加依賴:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
}
基本使用示例:
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
println("程序開始")
// 啟動一個協(xié)程
GlobalScope.launch {
delay(1000) // 非阻塞延遲 1 秒(協(xié)程特有的掛起函數)
println("協(xié)程執(zhí)行")
}
Thread.sleep(1500) // 讓主線程等待協(xié)程執(zhí)行完畢
println("程序結束")
}
runBlocking 可以創(chuàng)建一個阻塞當前線程的協(xié)程作用域,常用于橋接普通代碼和協(xié)程代碼:
fun main() = runBlocking {
println("程序開始")
launch {
delay(1000)
println("協(xié)程執(zhí)行")
}
println("程序結束")
// runBlocking 會等待所有子協(xié)程執(zhí)行完畢才會結束
}
3.3 協(xié)程的掛起函數
使用 suspend 關鍵字可以定義掛起函數,掛起函數只能在協(xié)程或其他掛起函數中調用:
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
// 定義掛起函數
suspend fun fetchData(): String {
delay(1000) // 模擬網絡請求
return "從服務器獲取的數據"
}
// 另一個掛起函數
suspend fun processData(): String {
delay(500) // 模擬數據處理
return "處理后的數據"
}
fun main() = runBlocking {
val data = fetchData()
println(data)
val processedData = processData()
println(processedData)
}
掛起函數的特點是在執(zhí)行到掛起點(如 delay)時,會暫停當前協(xié)程的執(zhí)行,讓出線程給其他協(xié)程,當掛起操作完成后,協(xié)程會在適當的時候恢復執(zhí)行。
3.4 協(xié)程的作用域與取消
協(xié)程應該在合適的作用域(CoroutineScope)中啟動,以便管理其生命周期。除了 GlobalScope,我們還可以創(chuàng)建自己的作用域:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
// 創(chuàng)建一個協(xié)程作用域
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
repeat(10) { i ->
println("正在執(zhí)行: $i")
delay(500)
}
}
delay(1500) // 等待 1.5 秒
job.cancel() // 取消協(xié)程
job.join() // 等待協(xié)程完全取消
println("協(xié)程已取消")
scope.cancel() // 取消整個作用域
}
輸出結果:
正在執(zhí)行: 0
正在執(zhí)行: 1
正在執(zhí)行: 2
協(xié)程已取消
4. 枚舉類
枚舉類(Enum Classes)用于表示固定數量的常量集合,如星期幾、方向、狀態(tài)等。
4.1 enum class 定義枚舉
// 簡單的枚舉類
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
4.2 枚舉常量的屬性與方法
枚舉常量可以有自己的屬性和方法:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF); // 注意這里的分號,當枚舉有成員函數時必須添加
fun isDark(): Boolean {
// 簡單判斷:RGB 值較低的認為是深色
return rgb < 0x808080
}
}
fun main() {
println(Color.RED.rgb) // 輸出: 16711680
println(Color.BLUE.isDark()) // 輸出: true
}
枚舉類還提供了一些內置方法:
-
valueOf(): 根據名稱獲取枚舉常量 -
values(): 返回所有枚舉常量的數組
fun main() {
val color = Color.valueOf("GREEN")
println(color) // 輸出: GREEN
for (c in Color.values()) {
println("$c: ${c.rgb}")
}
}
4.3 枚舉的應用場景
枚舉類適用于表示有限的、固定的選項集合:
- 狀態(tài)標識:表示對象的不同狀態(tài)
enum class OrderStatus {
CREATED, PAID, SHIPPED, DELIVERED, CANCELLED
}
- 選項列表:表示 UI 中的下拉選項等
enum class SortOption(val displayName: String) {
PRICE_ASC("價格從低到高"),
PRICE_DESC("價格從高到低"),
NEWEST("最新上架"),
POPULAR("最受歡迎")
}
- 替代魔法數字:使代碼更具可讀性
// 不推薦:使用魔法數字
if (status == 2) { ... }
// 推薦:使用枚舉
if (status == OrderStatus.SHIPPED) { ... }
5. 注解
注解(Annotations)是一種為代碼添加元數據的方式,它不會直接影響代碼的執(zhí)行,但可以被編譯器或其他工具使用。
5.1 注解的定義
使用 annotation class 關鍵字定義注解:
// 定義一個簡單的注解
annotation class MyAnnotation
// 帶參數的注解
annotation class Todo(val description: String)
// 限制注解的使用范圍
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class DeprecatedFeature(val replacement: String)
@Target 注解用于指定注解可以應用的元素類型,常見的目標包括:
-
CLASS: 類、接口、枚舉等 -
FUNCTION: 函數 -
PROPERTY: 屬性 -
PARAMETER: 函數參數 -
TYPE: 類型
還可以使用 @Retention 注解指定注解的保留策略:
-
SOURCE: 只在源代碼中保留,編譯后丟棄 -
BINARY: 保留到編譯后的字節(jié)碼中,但不會被虛擬機加載 -
RUNTIME: 保留到運行時,可以通過反射獲取
import kotlin.annotation.RetentionPolicy
@Retention(AnnotationRetention.RUNTIME)
annotation class DebugInfo
5.2 常用內置注解
Kotlin 提供了一些常用的內置注解:
-
@JvmStatic:用于 companion object 中的函數,使其編譯為 Java 中的靜態(tài)方法
class Utils {
companion object {
@JvmStatic
fun doSomething() {
// ...
}
}
}
-
@Deprecated:標記已過時的代碼
@Deprecated(
message = "此方法已過時,請使用 newMethod()",
replaceWith = ReplaceWith("newMethod()"),
level = DeprecationLevel.WARNING
)
fun oldMethod() {
// ...
}
-
@Nullable和@NonNull:標記變量或參數是否可以為 null,常用于與 Java 互操作
import org.jetbrains.annotations.Nullable
fun processData(@Nullable data: String?) {
// ...
}
-
@Suppress:抑制編譯器警告
@Suppress("UNCHECKED_CAST")
fun unsafeCast(obj: Any): String {
return obj as String
}
5.3 注解的應用
注解在實際開發(fā)中有很多應用場景:
-
代碼標記:如
@Deprecated標記過時代碼,@Test標記測試方法等 - 代碼生成:許多框架使用注解來生成代碼,如 Dagger、Room 等
// Room 數據庫框架使用注解
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "user_name") val name: String
)
- 運行時處理:通過反射在運行時獲取注解信息,實現特定邏輯
確保項目中添加了 Kotlin 反射依賴:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
}
代碼:
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class LogExecution
// 使用注解
class Service {
@LogExecution
fun performTask() {
// ...
}
}
// 處理注解的工具類
object AnnotationProcessor {
fun process(obj: Any) {
obj::class.memberFunctions.forEach { function ->
if (function.findAnnotation<LogExecution>() != null) {
println("執(zhí)行方法: ${function.name}")
// 可以在這里添加日志記錄、性能監(jiān)控等邏輯
}
}
}
}
fun main() {
val service = Service()
AnnotationProcessor.process(service)
service.performTask()
}
6. 其他實用進階特性
6.1 解構聲明
解構聲明允許將一個對象的多個屬性同時賦值給多個變量:
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("Alice", 30)
// 解構聲明
val (name, age) = person
println("$name 今年 $age 歲") // 輸出: Alice 今年 30 歲
// 對集合使用解構
val list = listOf("a", "b", "c")
val (first, second) = list
println("$first, $second") // 輸出: a, b
}
6.2 密封類
密封類(Sealed Classes)用于表示受限的類層次結構,適合用于定義枚舉類的擴展:
sealed class Result<out T>
data class Success<out T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
fun fetchData(): Result<String> {
return try {
// 模擬網絡請求
Success("獲取的數據")
} catch (e: Exception) {
Error(e.message ?: "未知錯誤")
}
}
fun main() {
val result = fetchData()
// 使用 when 表達式處理密封類,編譯器會檢查所有可能的情況
when (result) {
is Success -> println("成功: ${result.data}")
is Error -> println("失敗: ${result.message}")
}
}