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 块里做了校验)。

我的建议:内联类适合包装那些有业务含义的基础类型,比如金额、百分比、ID、电话号码等。不要什么类型都包,过度设计反而增加复杂度。

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 避坑指南

内联类虽然好用,但有几个坑要注意。我曾经踩过,分享给你:

我曾经犯过的错:
  1. 内联类不能有 init 块之外的属性——只能有一个属性,就是 value。别想着加第二个字段。
  2. 内联类不能继承其他类——它只能实现接口,不能继承类。因为内联后没有对象头,没法放虚函数表。
  3. 内联类在 Java 中会被擦除——如果你在 Java 代码里调用 Kotlin 的内联类,它会被当成原始类型。所以跨语言调用时要小心。
  4. 扩展函数不要和内联类的方法重名——内联类自己的方法优先级更高,扩展函数会被隐藏。容易造成混淆。

20.6 知识体系图

下面这张图总结了内联类与扩展函数在 API 设计中的核心关系:

内联类 + 扩展函数:优雅API设计 内联类 (Inline Class) @JvmInline value class 零开销类型包装 编译后内联为原始类型 只能有一个属性 (value) 扩展函数 (Extension) fun Type.functionName() 给已有类添加新方法 静态解析,非动态派发 不修改原始类 组合 组合效果:类型安全 + 流畅API + 零开销 例:99.99.toAmount().plus(10.00.toAmount()) 金额/百分比包装 类型安全,防传参错误 UI状态管理 UserId / DisplayName 领域驱动设计 业务语义显式化

20.7 总结

内联类和扩展函数,单独用已经很强了。但组合起来,才是真正的优雅 API 设计之道。

我个人习惯在项目里这样用:

  • 用内联类包装所有有业务含义的基础类型(金额、ID、百分比等)
  • 用扩展函数给这些内联类添加领域方法(加减乘除、格式化、校验等)
  • 在 ViewModel 或 UseCase 层,用这些类型化的参数,让代码自文档化

说白了,好的 API 设计,就是让调用者「想错都难」。内联类 + 扩展函数,正好帮我们做到这一点。

一句话总结:内联类提供类型安全,扩展函数提供流畅调用。两者结合,零成本打造优雅 API。

公众号:蓝海资料掘金营,微信deep3321