KMP 语法要点:expect/actual 与源集

要点

开山篇讲了 KMP 的定位与工程结构,本篇把”共享层”的语法吃透,只回答三个问题:源集(source set)怎么决定”哪段代码在哪个目标上编译”、expect/actual 的规则与三种形态、语言层边界——哪些 API 在 commonMain 里天然不存在。版本基线:Kotlin 2.2 稳定线(2025-06 GA),2.1.x 仍是存量主力。速查见同系列参考字典,本篇讲”为什么”,字典管”忘了来查”。

一、源集:不是目录约定,是编译边界

先说死最容易误判的一点:commonMain、androidMain、iosMain 不是三份平行工程,而是一条编译边界的链。Kotlin Gradle 插件自 1.9.20 起自动应用默认层级模板,目标之间的公共层自动合成中间源集。Android 加三个 iOS 目标(iosArm64 真机、iosSimulatorArm64 Apple Silicon 模拟器、iosX64 Intel 模拟器)后,结构如下:

composeApp/src/
├── commonMain/kotlin/    # 所有目标共享:模型、业务规则、Compose UI
├── iosMain/kotlin/       # 中间源集:三个 iOS 目标自动共享(模板自动合成)
└── androidMain/kotlin/   # Android 目标独有:MainActivity 等平台件
源集 编译到哪 能引用什么
commonMain 全部目标各编译一份 纯 Kotlin,以及声明支持多平台的库(Ktor、kotlinx-serialization 等)
iosMain 三个 iOS 目标 commonMain 的一切 + platform.*(Foundation、UIKit 等 Kotlin/Native API)
androidMain Android 目标 commonMain 的一切 + android.* 与大部分 java.*

反推一件事:commonMain 里每个文件会对着每个目标各编译一次,任何目标不认识的 API 就整体编译失败。”把 UIKit 调用塞进 commonMain 靠运行时判断绕过去”不是难,是不可能——平台泄漏在编译期被拦下,而不是上线后崩给你看。⚠️ 常见坑:新目标加进 targets 后,commonMain 里已有的每个 expect 都要补对应 actual,契约按目标数量倍增——expect 必须克制,只集中在平台能力的薄薄一层。

二、expect/actual:四条规则与三种形态

expect/actual 是编译期契约:commonMain 用 expect 声明”每个目标都得有”,各平台源集用 actual 兑现。规则不多,每条都由编译器强制:

规则 内容 违反的后果
expect 不写实现 只声明签名,不许有实现体 编译报错 Expected declaration must not have a body
actual 逐字对齐 同包、同名、同签名;默认参数值只能写在 expect 侧 编译失败,点名不匹配的声明
每个目标必须有 actual 缺一个目标就缺一个编译单元 编译失败,点名缺哪个目标
actual typealias 可顶替 expect class 可由平台现成类直接顶替 类型必须完全一致,不能只是长得像

三种形态与两个平台的 actual,一段看全:

// commonMain/Platform.kt —— 只写契约,没有一行实现
expect fun platformName(): String              // 形态一:函数
expect class TaskQueue {                       // 形态二:类,只声明成员签名
    fun enqueue(job: () -> Unit)
}
expect class Counter(value: Int) { fun addAndGet(delta: Int): Int }

// androidMain/Platform.android.kt
import android.os.Build
actual fun platformName(): String = "Android ${Build.VERSION.RELEASE}"
actual typealias Counter = java.util.concurrent.atomic.AtomicInteger  // 平台现成类直接顶替

// iosMain/Platform.ios.kt
import platform.UIKit.UIDevice
actual fun platformName(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
actual typealias Counter = kotlin.concurrent.AtomicInt   // Kotlin/Native 自带原子类,签名恰好逐字一致

两个容易漏判的细节:其一,函数与属性的 expect/actual 完全稳定,类(class)的仍带 Beta 提示,公开 API 要留升级余地;其二,actual 不能重复 expect 侧的默认参数,默认值写一处各平台共享。反模式点名:用 expect/actual 携带业务逻辑——三个 actual 各有业务分支,说明差异没抽象掉。actual 应短到一眼看完,出现 if 分支和状态字段就要警惕。

三、语言层边界:commonMain 里没有的东西

会写 Android 的 Kotlin,进 commonMain 第一天就会撞墙,因为一大批”理所当然”的 API 不在:

需求 JVM/Android 的直觉写法 commonMain 的正确写法
时间与格式化 java.time、SimpleDateFormat kotlinx-datetime:Clock.System.now()、Instant/LocalDate,格式化取字段自己拼
正则 java.util.regex kotlin.text.Regex,全目标可用
JSON 序列化 Gson/Jackson(靠反射) kotlinx-serialization(编译期生成,无反射也能跑)
反射改字段 kotlin-reflect Kotlin/Native 没有反射,换显式映射或 sealed class 分派
异步并发 协程 + Flow 原样可用——多平台库,一行不改跨端

记住两行的分量:协程与 Flow 在 commonMain 是原生公民,Flow<List<Task>>、suspend fun 随便写;而”没有反射”影响的是选库标准——靠反射的 JVM 库(Gson、Retrofit、大部分注解处理器)进不了 commonMain。⚠️ 常见坑:JVM 目标上 java.time 可用,于是”Android 上能编译、到 iOS 才炸”的代码很常见。纪律是业务逻辑从第一行起就写在 commonMain,让 iOS 编译器替你把关。

四、互操作注解:给 iOS 侧一个体面的 API

Kotlin 编译成 iOS 框架后,Swift 侧看到的是 Objective-C 头文件。三个注解决定”Swift 同事拿到的 API 长什么样”——其中 @Throws 是安全项:不标它,Kotlin 异常穿过 Swift 边界直接崩溃且无法被 catch:

import kotlin.Throws
import kotlin.native.HiddenFromObjC

@Throws(TaskParseException::class)          // 列出的异常映射为 NSError,Swift 可 try/catch
fun parseTasks(raw: String): List<Task> = json.decodeFromString(raw)

@HiddenFromObjC   // Kotlin 1.8.0+;内部工具类不给 Swift 生成接口
internal class TaskCache { /* ... */ }
注解 作用 有什么坑
@Throws 把列出的 Kotlin 异常映射为 NSError,Swift 可 try/catch 只对标注的类型生效,没标就抛 = 崩溃
@HiddenFromObjC 声明不进 Obj-C 头文件,Swift 也看不到 需 @OptIn(ExperimentalObjCRefinement::class);想”留给 Swift”该用下一个
@ShouldRefineInSwift 提示 Swift 侧用 refinement 包装替换原始声明 只是提示,包装代码要 Swift 侧自己写,忘写等于没标

优先级:@Throws 守住所有会被 Swift 调用的业务函数,再考虑后两个 API 洁癖注解。

这篇教会了什么,下一步去哪

收束:源集是编译边界,commonMain 对每个目标各编译一次;expect/actual 是编译期契约,typealias 可顶替现成实现;语言层边界记三样——java.time 换 kotlinx-datetime、反射换 kotlinx-serialization、协程 Flow 原样跨端;@Throws 是 iOS 侧的安全底线。下一步:参考字典速查配置与库坐标,双端待办实战把语法放进可跑的工程;Kotlin 本体补课去Kotlin 语法要点,每一项在 commonMain 原样成立。

版本核实(以官方文档为准):Kotlin 2.2.0 于 2025-06 发布,小版本以 kotlinlang.org 发布页为准;KMP 与默认层级模板自 Kotlin 1.9.20(2023-11)起为现行行为;expect/actual 规则与 @Throws、@HiddenFromObjC、@ShouldRefineInSwift(1.8.0 引入)以 kotlinlang.org 互操作文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇