在 Kotlin 多平台开发(Kotlin Multiplatform, 简称 KMP)中,expect 和 actual 是一对协同工作的修饰符关键字,它们的核心作用是:解决跨平台通用代码与平台特有 API 之间的调用与实现问题。
它们构成了 Kotlin 实现"跨平台代码共享,同时保留原生平台能力"的基础机制。
一、各自的具体分工
1、expect(期望声明)
写在通用代码模块(commonMain)中。
作用:用来定义规范和接口。它告诉 Kotlin 编译器:"我在通用代码里需要使用某种功能/类/函数,但我只在这里声明它的接口和签名,具体的实现逻辑交由各个具体平台去完成。"
特点:expect 声明的方法或类不能写具体的实现代码(没有函数体)。
2、actual(具体实现)
写在特定平台模块(如 androidMain、iosMain、jvmMain、jsMain 等)中。
作用:用来提供具体实现。它告诉 Kotlin 编译器:"这是针对特定平台,对 commonMain 中那个 expect 声明的具体实现逻辑。"
特点:必须与 expect 的名称、包路径、参数类型和返回值完全匹配。
二、代码示例
-
声明平台特有函数(以获取设备名称为例)
通用模块 (commonMain):// 声明一个期待在平台层实现的函数
expect fun getDeviceName(): String// 通用代码可以直接调用这个 expect 函数
fun printDeviceInfo() {
println("Current device: ${getDeviceName()}")
}
Android 模块 (androidMain):
// 提供 Android 端的具体实现
actual fun getDeviceName(): String {
return "Android " + android.os.Build.MODEL
}
iOS 模块 (iosMain):
import platform.UIKit.UIDevice
// 提供 iOS 端的具体实现
actual fun getDeviceName(): String {
return UIDevice.currentDevice.systemName + " " + UIDevice.currentDevice.systemVersion
}
- 利用类型别名(Typealias)直接绑定原生类
actual 不仅可以写函数体,还可以通过 typealias 直接复用平台已有的原生类,无需重复封装:
通用模块 (commonMain):
expect class AtomicInt(initialValue: Int) {
fun get(): Int
fun incrementAndGet(): Int
}
JVM / Android 模块 (jvmMain):
// 直接将 JVM 原生的 AtomicInteger 绑定位 actual 实现
import java.util.concurrent.atomic.AtomicInteger
actual typealias AtomicInt = AtomicInteger
三、核心特点与优势
1、零运行时开销(Zero Overhead)
与传统的依赖注入(DI)或运行时条件判断(if (isAndroid))不同,expect/actual 是在编译期(Compile Time)完成链接与替换的。编译后的平台产物中完全没有多余的抽象层或反射,性能与原生代码一致。
2、类型安全
编译器会强制检查。如果某个平台(如 iosMain)忘记写对应的 actual 实现,或者参数/返回值类型与 expect 不匹配,项目在编译阶段就会直接报错,保证不会把缺失实现的代码带到运行时。
3、不仅限于函数
expect/actual 机制可以修饰:
函数(fun)
类与接口(class / interface)
属性(val / var)
枚举(enum class)
类型别名(typealias)
四、最佳实践建议
尽管 expect/actual 很强大,但在实际 KMP 项目开发中,建议不要过度使用它:
优先抽象接口(Interface):对于复杂的业务模块(如网络库、数据库、蓝牙等),建议在 commonMain 中定义标准 Kotlin interface,然后在各平台实现,通过依赖注入(如 Koin)或构造函数传递。
何时使用 expect/actual:适合用于底层基础 API(如获取系统时间、UUID 生成、平台日志打印、文件路径获取等)或绑定平台已有工具类。