Skip to content

expect/actual 机制详解

源:Kotlin Multiplatform - Expected and Actual Declarations

expect/actual 是 Kotlin Multiplatform 的核心机制,它允许在共享代码中声明平台相关的 API,并在各平台分别实现。这是一种编译时多态,而非运行时依赖注入。

基本概念

expect/actual 的核心语法非常简单:

kotlin
// 声明平台相关的 API
expect fun getPlatformName(): String

expect class PlatformContext

fun greet(): String = "Hello from ${getPlatformName()}"
kotlin
import android.content.Context

actual fun getPlatformName(): String = "Android"

actual class PlatformContext(val context: Context)
kotlin
import platform.UIKit.UIDevice

actual fun getPlatformName(): String = 
    UIDevice.currentDevice.systemName

actual class PlatformContext

expect/actual 的使用场景

1. 平台 API 访问

当需要访问平台特定的系统能力时:

kotlin
// commonMain
expect fun getCurrentTimestamp(): Long

// androidMain
actual fun getCurrentTimestamp(): Long = 
    System.currentTimeMillis()

// iosMain
import platform.Foundation.NSDate

actual fun getCurrentTimestamp(): Long = 
    (NSDate().timeIntervalSince1970 * 1000).toLong()

2. 平台特定类型封装

封装平台类型为共享类型:

kotlin
// commonMain
expect class File {
    fun readText(): String
    fun writeText(text: String)
}

// androidMain
import java.io.File as JvmFile

actual class File(private val file: JvmFile) {
    actual fun readText(): String = file.readText()
    actual fun writeText(text: String) = file.writeText(text)
}

// iosMain
import platform.Foundation.*

actual class File(private val path: String) {
    actual fun readText(): String {
        return NSString.stringWithContentsOfFile(
            path, 
            encoding = NSUTF8StringEncoding, 
            error = null
        ) as String
    }
    
    actual fun writeText(text: String) {
        (text as NSString).writeToFile(
            path,
            atomically = true,
            encoding = NSUTF8StringEncoding,
            error = null
        )
    }
}

3. 平台工厂模式

创建平台特定的实例:

kotlin
// commonMain
interface Logger {
    fun log(message: String)
}

expect fun createLogger(): Logger

// androidMain
import android.util.Log

actual fun createLogger(): Logger = object : Logger {
    override fun log(message: String) {
        Log.d("KMP", message)
    }
}

// iosMain
import platform.Foundation.NSLog

actual fun createLogger(): Logger = object : Logger {
    override fun log(message: String) {
        NSLog(message)
    }
}

高级用法

expect 类与构造函数

kotlin
// commonMain
expect class Database {
    constructor(name: String)
    
    fun query(sql: String): List<Map<String, Any?>>
    fun close()
}

// androidMain
import android.database.sqlite.SQLiteDatabase

actual class Database actual constructor(name: String) {
    private val db: SQLiteDatabase = TODO("实现细节")
    
    actual fun query(sql: String): List<Map<String, Any?>> {
        // Android SQLite 实现
        TODO()
    }
    
    actual fun close() {
        db.close()
    }
}

expect 接口

kotlin
// commonMain
expect interface Parcelable

data class User(val name: String) : Parcelable

// androidMain
actual typealias Parcelable = android.os.Parcelable

// iosMain
actual interface Parcelable // 空接口

泛型 expect/actual

kotlin
// commonMain
expect class AtomicReference<T> {
    constructor(value: T)
    
    fun get(): T
    fun set(value: T)
    fun compareAndSet(expect: T, update: T): Boolean
}

// androidMain (JVM)
import java.util.concurrent.atomic.AtomicReference as JvmAtomicReference

actual class AtomicReference<T> actual constructor(value: T) {
    private val ref = JvmAtomicReference(value)
    
    actual fun get(): T = ref.get()
    actual fun set(value: T) = ref.set(value)
    actual fun compareAndSet(expect: T, update: T): Boolean =
        ref.compareAndSet(expect, update)
}

// iosMain (Native)
import kotlin.native.concurrent.AtomicReference as NativeAtomicReference

actual class AtomicReference<T> actual constructor(value: T) {
    private val ref = NativeAtomicReference(value. freeze())
    
    actual fun get(): T = ref.value
    actual fun set(value: T) { ref.value = value.freeze() }
    actual fun compareAndSet(expect: T, update: T): Boolean =
        ref.compareAndSet(expect.freeze(), update.freeze())
}

expect 属性

kotlin
// commonMain
expect val isDebug: Boolean

// androidMain
actual val isDebug: Boolean = BuildConfig.DEBUG

// iosMain
actual val isDebug: Boolean = true // 或从 Info.plist 读取

中间源集的 expect/actual

对于共享部分平台逻辑的情况,可以使用中间源集:

kotlin
// commonMain
expect class Clipboard {
    fun copy(text: String)
    fun paste(): String?
}

// appleMain (iOS + macOS 共享)
import platform.UIKit.UIPasteboard // iOS
import platform.AppKit.NSPasteboard // macOS

actual class Clipboard {
    actual fun copy(text: String) {
        #if iOS
        UIPasteboard.generalPasteboard.string = text
        #else
        NSPasteboard.generalPasteboard.setString(text, forType = NSPasteboardTypeString)
        #endif
    }
    
    actual fun paste(): String? {
        #if iOS
        return UIPasteboard.generalPasteboard.string
        #else
        return NSPasteboard.generalPasteboard.stringForType(NSPasteboardTypeString)
        #endif
    }
}

// androidMain
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context

actual class Clipboard(private val context: Context) {
    private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    actual fun copy(text: String) {
        clipboard.setPrimaryClip(ClipData.newPlainText("", text))
    }
    
    actual fun paste(): String? {
        return clipboard.primaryClip?.getItemAt(0)?.text?.toString()
    }
}

常见陷阱与最佳实践

❌ 陷阱 1:expect 声明与 actual 实现签名不匹配

kotlin
// commonMain
expect fun fetchData(): String

// androidMain - 错误:返回类型不匹配
actual fun fetchData(): String? = null // ❌ 编译错误

解决方案:确保签名完全一致,或在 commonMain 中使用可空类型。

❌ 陷阱 2:expect 类中包含实现

kotlin
// commonMain - 错误
expect class Config {
    val name: String
    
    fun isValid(): Boolean = name.isNotEmpty() // ❌ expect 类不能有实现
}

解决方案:将实现逻辑移到 actual 类或普通类中。

❌ 陷阱 3:滥用 expect/actual

对于可以用纯 Kotlin 实现的逻辑,不应使用 expect/actual:

kotlin
// 不推荐
expect fun add(a: Int, b: Int): Int

// 推荐:直接在 commonMain 实现
fun add(a: Int, b: Int): Int = a + b

✅ 最佳实践 1:最小化 expect 声明

只在必要时使用 expect/actual,尽量让共享代码保持纯 Kotlin。

kotlin
// commonMain
interface Storage {
    fun save(key: String, value: String)
    fun load(key: String): String?
}

expect fun createStorage(): Storage

// 业务逻辑保持在 commonMain
class UserRepository(private val storage: Storage) {
    fun saveUser(user: User) {
        storage.save("user", user.toJson())
    }
}

✅ 最佳实践 2:使用工厂函数而非 expect 类

kotlin
// 更灵活的方式
interface HttpClient {
    suspend fun get(url: String): String
}

expect fun createHttpClient(): HttpClient

// 而非
expect class HttpClient {
    suspend fun get(url: String): String
}

✅ 最佳实践 3:为 expect 声明编写文档

kotlin
/**
 * 获取设备的唯一标识符
 * 
 * @return Android: ANDROID_ID, iOS: identifierForVendor
 */
expect fun getDeviceId(): String

✅ 最佳实践 4:单元测试覆盖

为每个平台的 actual 实现编写测试:

kotlin
class PlatformTest {
    @Test
    fun testGetPlatformName() {
        val name = getPlatformName()
        assertTrue(name.isNotEmpty())
    }
}
kotlin
class AndroidPlatformTest {
    @Test
    fun testPlatformNameContainsAndroid() {
        val name = getPlatformName()
        assertTrue(name.contains("Android", ignoreCase = true))
    }
}
kotlin
class IosPlatformTest {
    @Test
    fun testPlatformNameIsIOS() {
        val name = getPlatformName()
        assertEquals("iOS", name)
    }
}

expect/actual vs 其他方案对比

方案适用场景优点缺点
expect/actual访问平台 API,编译时确定类型安全,零运行时开销需要为每个平台提供实现
依赖注入可替换的业务逻辑灵活,便于测试运行时开销,需要 DI 框架
typealias平台类型别名简单直接仅适用于类型映射
接口 + 平台实现复杂业务逻辑解耦,易扩展需要手动注入

何时使用 expect/actual

适合使用的场景

  • ✅ 访问平台系统 API(文件、数据库、网络)
  • ✅ 平台特定的工具类(日志、加密)
  • ✅ 硬件访问(传感器、相机)
  • ✅ UI 平台差异(颜色、字体)

不适合使用的场景

  • ❌ 可以用纯 Kotlin 实现的逻辑
  • ❌ 需要运行时动态切换的实现
  • ❌ 复杂的业务规则(建议用接口 + DI)

编译器如何处理 expect/actual

  1. 编译 commonMain:编译器看到 expect 声明,但不需要实现
  2. 链接平台代码:编译各平台时,编译器验证每个 expect 都有匹配的 actual
  3. 生成平台代码:最终产物中,expect 声明被替换为对应平台的 actual 实现

编译时检查

  • 签名必须完全匹配(包括参数名、类型、修饰符)
  • 所有 expect 必须有对应的 actual
  • actual 不能比 expect 有更宽松的可见性

通过合理使用 expect/actual 机制,可以在保持共享代码最大化的同时,充分利用各平台的原生能力。