expect/actual — 本质、KMM关键字及工作原理

作者: IT Sectr 发布日期: 2026-06-05 阅读时间: 8 分钟

expect/actual — Kotlin Multiplatform的机制,允许在通用代码中声明平台相关的API。关键字expect在commonMain中创建函数、类或属性的约定,而关键字actual为每个平台提供具体实现。编译器检查每个expect声明在所有目标平台上都有对应的actual实现。根据JetBrains, 2025的数据,80%的KMM项目使用该机制实现平台业务逻辑。

要点

  • expect — 用于在通用代码中声明函数、类或属性约定的关键字。
  • actual — 用于提供expect声明平台实现的关键字。
  • commonMain — 包含expect声明的通用代码source set。
  • 编译器检查 — 编译器保证所有目标平台都有actual实现。
  • Source set — 包含平台actual实现的集合(iosMain, androidMain)。

什么是expect/actual?

expect/actual — 是Kotlin Multiplatform用于实现面向平台编程的声明式机制。它允许在通用模块中描述一次API(expect),并为每个平台单独实现(actual)。与接口不同,expect/actual不会创建虚拟调用——编译器在编译阶段连接expect和actual声明,消除了动态分派的开销。

expect/actual的历史始于2017年Kotlin Multiplatform的出现。最初该机制被称为expect/actual declarations,是实验性的。在Kotlin 1.2中添加了expect注解,在Kotlin 1.3中expect/actual对类和函数成为稳定特性。该机制逐步扩展:Kotlin 1.6增加了对companion对象的expect/actual支持,Kotlin 1.7——对enum类,Kotlin 2.0——对typealias。

expect/actual的关键特性是编译级安全性。如果开发者在commonMain中添加了expect声明但忘记为iOS提供actual实现,编译器将报错。这防止了反射或动态加载平台代码方法中典型的运行时错误。

expect/actual机制的工作原理

expect/actual机制在source set级别工作——Kotlin Multiplatform的模块系统。所有平台可访问的通用代码位于commonMain source set中。平台相关代码——在iosMain、androidMain、macosMain等中。关键字expect在commonMain中声明API,而关键字actual在平台source set中提供实现。编译器在代码生成阶段将它们连接起来,将对expect函数的调用替换为目标平台对应的actual实现。

典型KMM项目中的source set层次结构如下:commonMain包含expect声明,iosMainandroidMain包含actual实现。编译iOS时使用iosMain中的actual,编译Android时使用androidMain中的actual。Source set可以是中间级别的(例如针对特定架构的iosArm64Main),从而允许为不同设备细化实现。

kotlin
// commonMain — expect声明
expect fun getPlatformName(): String

// androidMain — Android的actual
actual fun getPlatformName(): String = "Android"

// iosMain — iOS的actual
actual fun getPlatformName(): String = "iOS"

编译器对actual实现的检查

Kotlin编译器在处理expect/actual时检查几个条件。每个expect声明必须为每个活动平台具有actual实现。actual声明的签名必须与expect签名匹配(@OptionalExpectation注解可以放宽此要求)。访问修饰符、返回类型和参数必须相同。编译器还检查expect和actual声明之间没有循环依赖。

expect/actual类型:函数、类、属性

expect/actual支持多种声明类型。最常用的是expect/actual函数用于平台操作,expect/actual类用于需要原生实现的对象,以及expect/actual属性用于常量和设置。每种类型都有自己的使用规则和限制。

Expect/actual函数——最简单和最常用的类型。它们用于调用平台API,如获取时间、读取文件或发送HTTP请求。Expect/actual类用于创建直接与原生代码交互的对象(例如访问相机、地理位置或安全存储)。Expect/actual属性(val)适用于平台常量——操作系统名称、SDK版本或系统目录路径。

声明类型关键字使用示例
函数expect fun / actual fun获取设备唯一标识符
expect class / actual class访问SecureStorage(Keychain / EncryptedSharedPreferences)
属性expect val / actual val当前平台(iOS / Android)
枚举类expect enum / actual enum可用应用权限列表
类型别名expect typealias / actual typealias平台特定的网络响应类型

expect/actual的限制

并非所有Kotlin构造都可以与expect/actual一起使用。Expect声明不能包含主体——只有签名。Expect类不能有带参数的构造函数(必须为空的主构造函数)。对于enum expect/actual,所有常量在expect和actual中必须相同。Expect属性必须是val(而非var),因为在通用模块中为平台属性存储状态没有意义。

代码示例:从简单到复杂

让我们看看从简单函数到完整类的expect/actual实际示例。基本案例——获取平台名称用于UI。更复杂的示例包括访问原生存储和处理平台线程。

kotlin
// commonMain — 安全存储的expect类
expect class PlatformStorage {
    fun save(key: String, value: String)
    fun get(key: String): String?
    fun remove(key: String)
}

// androidMain — Android上的actual
actual class PlatformStorage {
    private val prefs = AppContext.getSharedPreferences("secure", 0)

    actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    actual fun get(key: String): String? = prefs.getString(key, null)
    actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}

在这个示例中,expect类PlatformStorage定义了一个简单键值存储的约定。在Android上,实现使用SharedPreferences;在iOS上——Keychain或NSUserDefaults。得益于expect/actual,commonMain中的业务逻辑调用save/get/remove,而无需了解平台实现。

kotlin
// iosMain — 使用Keychain在iOS上的actual
actual class PlatformStorage {
    actual fun save(key: String, value: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecValueData to value.encodeToByteArray()
        )
        SecItemAdd(query, null)
    }

    actual fun get(key: String): String? {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecReturnData to true
        )
        val result = mutableMapOf<String, Any>()
        return if (SecItemCopyMatching(query, result) == errSecSuccess)
            result[kSecValueData]?.toString()
        else null
    }

    actual fun remove(key: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key
        )
        SecItemDelete(query)
    }
}

expect/actual最佳实践

在设计expect/actual API时应遵循几个原则。最小化expect声明的数量——通用代码越多,维护越容易。只对那些在平台间确实不同的API使用expect/actual。对于其余代码使用带有工厂或依赖注入的接口,这简化了测试。

建议按主题模块对expect声明进行分组,而不是混合在同一个文件中。例如,Storage.kt用于存储的expect声明,Platform.kt用于操作系统相关的expect函数,Analytics.kt用于分析类的expect声明。这简化了KMM项目平台表面的导航和理解。每个actual文件应位于相应的source set中:androidMain、iosMain、desktopMain等。

通过expect fun与actual fun(其中actual使用通用代码)实现默认实现是一种常见的反模式。如果平台实现与默认实现没有区别,则不需要expect/actual。在这种情况下,在commonMain中使用简单函数。同时避免对简单的getter使用expect/actual——使用带有常量的expect val。

项目中的代码组织

正确的expect/actual代码结构对项目的可读性至关重要。每个expect/actual模块应有一个单一的入口点。组织示例:commonMain/kotlin/com/project/platform包含expect声明,androidMain/kotlin/com/project/platform——Android的actual,iosMain/kotlin/com/project/platform——iOS的actual。文件和包名称在expect和actual中应一致,以便开发者快速找到相应的实现。

KMM中expect/actual的替代方案

带有平台工厂的接口——expect/actual的主要替代方案。不必使用expect类,可以在commonMain中声明接口,并在平台模块中创建具体类。工厂或依赖注入容器在运行时提供正确的实现。这种方法更适合测试,因为接口可以被模拟。

依赖注入(Koin、Kodein)——更灵活但性能较低的方法。DI容器为每个平台单独配置,并向通用代码提供平台依赖。与expect/actual不同,注入发生在运行时,允许替换实现以进行测试。另一方面,DI配置错误只有在运行时才会被发现,而不是在编译阶段。

方法编译时检查测试灵活性运行时开销
expect/actual完全低(actual无法模拟)零(编译时绑定)
接口 + 工厂部分高(可模拟)最小(虚拟调用)
依赖注入无(运行时)中等(DI代理)

expect/actual与替代方案之间的选择取决于上下文。对于关键性能(游戏引擎、实时处理),expect/actual因零开销而更受青睐。对于业务逻辑(仓库、用例),最好使用带有DI的接口以简化测试。组合方法——expect/actual用于底层平台操作,接口用于业务逻辑层——在大多数生产级KMM项目中采用。

常见问题

expect/actual与接口有什么区别?

expect/actual在编译阶段绑定实现而不需要虚拟调用,而接口在运行时绑定。expect/actual保证所有平台都有实现,接口需要运行时检查。

可以对枚举使用expect/actual吗?

是的,从Kotlin 1.7开始支持expect enum。expect和actual枚举中的所有常量必须相同。不同平台上常量值不同是编译错误。

如果忘记提供actual实现会怎样?

编译器会为每个缺少actual实现的平台报错。在为所有expect声明添加相应的actual实现之前,项目将无法构建。

可以在同一个source set中使用expect/actual吗?

不可以。expect和actual必须位于不同的source set中。expect在commonMain或中间source set中,actual在平台source set中。将expect和actual放在同一个source set中是编译错误。

如何测试expect/actual代码?

使用带有平台测试source set的commonTest来测试expect/actual。在commonTest中编写expect测试,为每个平台编写actual测试。集成测试在每个目标平台上单独运行。

总结

  • expect/actual — Kotlin Multiplatform中带有编译器检查的平台实现的关键机制。
  • expect在commonMain中声明约定,actual在平台source set中提供实现。
  • 声明类型包括函数、类、属性、枚举类和类型别名,各有不同的使用规则。
  • 编译器检查保证所有目标平台都有actual实现,防止运行时错误。
  • 建议最小化expect/actual,对业务逻辑使用接口加DI。
  • 代码组织应保持统一,expect和actual的文件和包名称一致。
  • 对底层平台操作(存储、文件系统、传感器)使用expect/actual——确保零运行时开销。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读