序列、类型别名与内联值类

本章整理三个经常在真实项目中一起出现的 Kotlin 特性:Sequencetypealias 和 inline value class。它们都能改善代码表达,但解决的问题完全不同。

Sequence:惰性集合处理

Kotlin 集合操作默认是急切执行的:

val result = users
    .filter { it.active }
    .map { it.name }
    .take(10)

对于 List,每一步都会产生中间集合。数据量较小时,这通常没问题;数据量大、链条长、前面过滤后只需要少量结果时,可以考虑 Sequence

val result = users.asSequence()
    .filter { it.active }
    .map { it.name }
    .take(10)
    .toList()

Sequence 的中间操作是惰性的,只有终端操作触发时才真正执行。

Iterable 与 Sequence 的执行顺序

Iterable 会按步骤处理整个集合:

val words = "The quick brown fox jumps over the lazy dog".split(" ")

val lengths = words
    .filter { println("filter: $it"); it.length > 3 }
    .map { println("map: $it"); it.length }
    .take(4)

Sequence 会尽量按元素推进:

val lengths = words.asSequence()
    .filter { println("filter: $it"); it.length > 3 }
    .map { println("map: $it"); it.length }
    .take(4)
    .toList()

take(4) 已经拿够结果时,后续元素不会继续处理。

创建 Sequence

从元素创建:

val numbers = sequenceOf(1, 2, 3)

从集合创建:

val numbers = listOf(1, 2, 3).asSequence()

用函数生成:

val oddNumbers = generateSequence(1) { it + 2 }
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]

有限生成:

val lessThanTen = generateSequence(1) { previous ->
    if (previous < 8) previous + 2 else null
}

sequence {}yield

val numbers = sequence {
    yield(1)
    yieldAll(listOf(2, 3))
    yieldAll(generateSequence(4) { it + 1 })
}

注意:如果 yieldAll() 接收无限序列,它必须是最后一步,否则后面的代码永远不会执行。

Sequence 的适用场景

适合:

  • 大集合多步处理。
  • 处理链条中存在 takefirst 等短路终端操作。
  • 数据是逐步生成的。
  • 希望避免中间集合分配。

不一定适合:

  • 小集合。
  • 只有一两步简单操作。
  • 每一步操作成本很低,Sequence 自身开销反而更明显。

Java 对比:Java Stream 也是惰性流水线。Kotlin Sequence 更像 Java Stream 的惰性处理能力,但 Kotlin 普通集合操作本身已经足够简洁,不需要每次都转成 Sequence。

typealias:给已有类型起别名

类型别名不会创建新类型,只是给已有类型起更清晰的名字:

typealias UserId = Long
typealias UserCache = MutableMap<UserId, User>

使用:

fun findUser(id: UserId): User? = cache[id]

UserId 本质上仍然是 Long

val id: UserId = 1L
val raw: Long = id // OK

typealias 适合什么

适合:

  • 简化复杂泛型类型。
  • 给函数类型命名。
  • 给嵌套类或长包名类型提供更短别名。

示例:

typealias CompletionHandler = (Result<User>) -> Unit

fun loadUser(callback: CompletionHandler) {
    // ...
}

不适合:

  • 需要真正类型安全的领域 ID。
  • 希望阻止 OrderIdUserId 混用。

因为:

typealias UserId = Long
typealias OrderId = Long

fun loadUser(id: UserId) {}

val orderId: OrderId = 100L
loadUser(orderId) // 可以编译,因为它们都是 Long

嵌套 typealias

Kotlin 允许在其他声明内部定义不捕获外层类型参数的 typealias。它适合把只服务于某个类或对象的复杂类型名收进局部作用域:

class Dijkstra {
    data class Node(val id: String)

    private typealias VisitedNodes = Set<Node>

    private fun step(visited: VisitedNodes) {
        println(visited.size)
    }
}

嵌套 typealias 的主要价值是封装:

  • 不污染包级命名空间。
  • 类型别名跟使用它的算法或组件放在一起。
  • 可以用 privateinternal 限制可见性。

但它不能捕获外层类的类型参数:

class Graph<Node> {
    // 错误:Path 捕获了外层 Graph<Node> 的 Node
    // typealias Path = List<Node>
}

正确做法是给 typealias 自己声明类型参数:

class Graph<Node> {
    typealias Path<N> = List<N>
}

规则总结:

  • 嵌套 typealias 仍然只是别名,不创建新类型。
  • 可见性不能比被引用的底层类型更宽。
  • 作用域类似嵌套类;同名别名会隐藏外层别名,但不是 override。
  • Kotlin Multiplatform 的 expect/actual 声明不支持嵌套 typealias。

Java 对比:Java 没有 typealias。Java 开发者常用小接口、包装类或静态内部类来缩短类型名,但这些通常会创建新类型。Kotlin 的 typealias 只影响源码可读性,不改变二进制模型。

内联值类:零成本领域类型

如果你想创建一个真正的新类型,同时尽量避免包装对象开销,可以使用 inline value class:

@JvmInline
value class UserId(val value: Long)

现在 UserIdLong 不再能随意混用:

fun loadUser(id: UserId) {}

loadUser(UserId(1L))
// loadUser(1L) // 编译错误

这非常适合领域 ID、金额单位、邮箱、密码、令牌等“底层是简单值,但语义不同”的类型。

内联值类的限制

内联值类必须:

  • 使用 value class
  • JVM 上使用 @JvmInline
  • 主构造函数中只有一个属性。
  • 不能继承类。
  • 默认是 final

它可以实现接口:

interface Printable {
    fun pretty(): String
}

@JvmInline
value class Email(val value: String) : Printable {
    init {
        require("@" in value)
    }

    override fun pretty(): String = value.lowercase()
}

内联值类的属性不能有 backing field,因此不能使用 lateinit 或委托属性。

装箱与运行时表示

内联值类在很多场景下会用底层值表示,减少分配:

@JvmInline
value class UserId(val value: Long)

运行时可能直接使用 long / Long。但在一些情况下会装箱,例如:

  • 作为泛型类型使用。
  • 作为接口类型使用。
  • 可空值类 UserId?
  • 需要运行时对象身份的场景。

因此它不是“永远不分配对象”的承诺,而是“编译器尽可能使用底层表示”。

内联值类与 Java 互操作

因为 JVM 上内联值类可能被编译成底层类型,函数签名可能发生冲突:

@JvmInline
value class UIntLike(val value: Int)

fun compute(value: Int) {}
fun compute(value: UIntLike) {}

编译器会对使用值类的函数名做 name mangling,避免 JVM 签名冲突。需要给 Java 调用方暴露稳定方法名时,可以使用 @JvmName

@JvmName("computeUIntLike")
fun compute(value: UIntLike) {}

公共 Java API 中使用值类要格外谨慎,必要时提供 Java 友好的重载或包装 API。

typealias vs inline value class

需求 推荐
简化长类型名 typealias
命名函数类型 typealias
创建真正不同的领域类型 inline value class
避免 UserIdOrderId 混用 inline value class
只想改善可读性,不改变类型系统 typealias

实践建议

  • 不要为了“性能”盲目使用 Sequence,小集合普通链式调用更简单。
  • Sequence 适合大数据、多步骤、短路处理。
  • typealias 是可读性工具,不提供类型安全。
  • 嵌套 typealias 适合内部算法和局部复杂类型名,不适合作为跨模块公共抽象的核心。
  • 领域 ID、金额、邮箱等建议优先考虑内联值类。
  • Java 互操作 API 中使用内联值类前,检查生成签名和调用体验。

参考

  • 官方序列文档:https://kotlinlang.org/docs/sequences.html
  • 官方类型别名文档:https://kotlinlang.org/docs/type-aliases.html
  • 官方内联值类文档:https://kotlinlang.org/docs/inline-classes.html