20. 内联类与扩展函数:优雅的API设计
说实话,Kotlin 里我最喜欢的特性之一,就是内联类和扩展函数的组合。这俩东西单独用已经很香了,但放在一起,简直就是 API 设计的「王炸」。我在好几个项目里靠这套组合拳,把原本又臭又长的调用代码,改得跟诗一样优雅。
今天我们就来聊聊,怎么用内联类 + 扩展函数,设计出让调用者「哇塞」的 API。
20.1 内联类:轻量级的类型包装
先说说内联类。它的核心思想很简单:用零开销的方式,给现有类型穿上新衣服。
你想想看,很多时候我们传参数,传的都是 String、Int 这些基础类型。但问题来了——
fun saveUser(name: String, email: String, phone: String) {
// 这三个 String 参数,传错顺序怎么办?
}
嗯,这里要注意。三个 String 参数,编译器根本不知道哪个是名字、哪个是邮箱、哪个是电话。你传反了,它也不报错。运行时才发现,那就晚了。
内联类就是来解决这个问题的:
@JvmInline
value class UserName(val value: String)
@JvmInline
value class Email(val value: String)
@JvmInline
value class Phone(val value: String)
fun saveUser(name: UserName, email: Email, phone: Phone) {
// 类型安全,传错顺序?编译器直接报错
}
调用的时候也很清晰:
saveUser(UserName("张三"), Email("zhangsan@example.com"), Phone("13800138000"))
我个人习惯把这种包装叫做「语义化类型」。它不增加运行时开销(因为编译后会被内联成原始类型),但让代码的可读性和安全性直接拉满。
20.2 扩展函数:给别人的类加方法
扩展函数大家应该不陌生。它允许你给已有的类添加新方法,哪怕这个类是第三方库里的。
fun String.isEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// 调用
"test@example.com".isEmail() // true
但扩展函数有个「坑」——它是静态解析的,不是动态派发。什么意思呢?
open class Animal
class Dog : Animal()
fun Animal.speak() = "动物叫"
fun Dog.speak() = "汪汪"
val pet: Animal = Dog()
println(pet.speak()) // 输出 "动物叫",不是 "汪汪"
为什么会这样?因为扩展函数本质上就是个静态的顶层函数,调用哪个版本取决于编译时的类型,不是运行时的实际类型。这一点要牢记。
20.3 组合拳:内联类 + 扩展函数
好了,重点来了。内联类和扩展函数组合起来,能做出什么效果?
我在项目中遇到过这样一个场景:需要处理各种金额计算。直接用 Double 或者 BigDecimal,代码里全是魔法数字,维护起来想哭。
用内联类 + 扩展函数改造后:
@JvmInline
value class Amount(val value: BigDecimal) {
init {
require(value >= BigDecimal.ZERO) { "金额不能为负数" }
}
}
// 扩展函数:从数字创建 Amount
fun Int.toAmount(): Amount = Amount(this.toBigDecimal())
fun Double.toAmount(): Amount = Amount(this.toBigDecimal())
fun String.toAmount(): Amount = Amount(this.toBigDecimal())
// 扩展函数:Amount 的运算
fun Amount.plus(other: Amount): Amount = Amount(this.value + other.value)
fun Amount.minus(other: Amount): Amount = Amount(this.value - other.value)
// 调用
val price = 99.99.toAmount()
val tax = 10.00.toAmount()
val total = price.plus(tax)
你看,调用代码变得非常自然。99.99.toAmount() 读起来就是「把 99.99 转成金额」。而且 Amount 类型保证了不会出现负数金额(init 块里做了校验)。
20.4 实战:Android 中的状态管理
在 Android 里,内联类 + 扩展函数最常用的场景之一,就是状态管理。
我以前写过一个项目,UI 状态用 sealed class 定义,但每个状态里都有一堆字段,代码很啰嗦。后来用内联类优化了部分字段:
// 用内联类包装 UI 状态中的关键字段
@JvmInline
value class UserId(val value: String)
@JvmInline
value class DisplayName(val value: String)
@JvmInline
value class AvatarUrl(val value: String)
// UI 状态
data class UserProfileState(
val userId: UserId,
val name: DisplayName,
val avatar: AvatarUrl,
val isLoading: Boolean = false,
val error: String? = null
)
// 扩展函数:方便创建和转换
fun UserProfileState.toUiModel(): UserProfileUiModel {
return UserProfileUiModel(
id = this.userId.value,
displayName = this.name.value,
avatarUrl = this.avatar.value
)
}
这样做的好处是:
- 类型安全:不会把 userId 和 avatarUrl 搞混
- 自文档化:看到 UserId 就知道它代表什么
- 零开销:编译后就是原始 String,没有额外对象
20.5 避坑指南
内联类虽然好用,但有几个坑要注意。我曾经踩过,分享给你:
- 内联类不能有 init 块之外的属性——只能有一个属性,就是 value。别想着加第二个字段。
- 内联类不能继承其他类——它只能实现接口,不能继承类。因为内联后没有对象头,没法放虚函数表。
- 内联类在 Java 中会被擦除——如果你在 Java 代码里调用 Kotlin 的内联类,它会被当成原始类型。所以跨语言调用时要小心。
- 扩展函数不要和内联类的方法重名——内联类自己的方法优先级更高,扩展函数会被隐藏。容易造成混淆。
20.6 知识体系图
下面这张图总结了内联类与扩展函数在 API 设计中的核心关系:
20.7 总结
内联类和扩展函数,单独用已经很强了。但组合起来,才是真正的优雅 API 设计之道。
我个人习惯在项目里这样用:
- 用内联类包装所有有业务含义的基础类型(金额、ID、百分比等)
- 用扩展函数给这些内联类添加领域方法(加减乘除、格式化、校验等)
- 在 ViewModel 或 UseCase 层,用这些类型化的参数,让代码自文档化
说白了,好的 API 设计,就是让调用者「想错都难」。内联类 + 扩展函数,正好帮我们做到这一点。
公众号:蓝海资料掘金营,微信deep3321