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 PlatformContextexpect/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
- 编译 commonMain:编译器看到
expect声明,但不需要实现 - 链接平台代码:编译各平台时,编译器验证每个
expect都有匹配的actual - 生成平台代码:最终产物中,
expect声明被替换为对应平台的actual实现
编译时检查:
- 签名必须完全匹配(包括参数名、类型、修饰符)
- 所有
expect必须有对应的actual actual不能比expect有更宽松的可见性
通过合理使用 expect/actual 机制,可以在保持共享代码最大化的同时,充分利用各平台的原生能力。