序列、类型别名与内联值类¶
本章整理三个经常在真实项目中一起出现的 Kotlin 特性:Sequence、typealias 和 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 的适用场景¶
适合:
- 大集合多步处理。
- 处理链条中存在
take、first等短路终端操作。 - 数据是逐步生成的。
- 希望避免中间集合分配。
不一定适合:
- 小集合。
- 只有一两步简单操作。
- 每一步操作成本很低,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。
- 希望阻止
OrderId和UserId混用。
因为:
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 的主要价值是封装:
- 不污染包级命名空间。
- 类型别名跟使用它的算法或组件放在一起。
- 可以用
private或internal限制可见性。
但它不能捕获外层类的类型参数:
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)
现在 UserId 和 Long 不再能随意混用:
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 |
避免 UserId 和 OrderId 混用 |
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