跳到主要内容

Kotlin 集合校验新姿势:告别 distinct().size 的低效写法

· 阅读需 6 分钟
重庆 Kotlin User Group

在日常开发中,我们经常需要校验集合中元素的唯一性或一致性——比如检查用户提交的 ID 是否有重复、验证配置项是否全部相同、或者确保一批数据满足某种约束。在 Kotlin 2.4.20 之前,这些操作往往需要借助 distinct().size 或 toSet().size 这样的间接写法。Kotlin 2.4.20 带来了一组语义清晰的集合校验函数,让代码意图一目了然。

旧写法:能跑,但不够好​

在 Kotlin 2.4.20 之前,检查集合元素是否全部唯一,最常见的写法是:

val ids = listOf("user_001", "user_002", "user_003")

// 检查是否有重复
val allUnique = ids.distinct().size == ids.size
// 或者
val noDuplicates = ids.toSet().size == ids.size

这些写法能工作,但有几个问题:

  1. 语义不直观:读者需要"翻译"一下才能理解意图——"去重后大小不变"等价于"没有重复"
  2. 性能浪费:distinct() 会创建一个全新的 List,toSet() 会创建一个 Set,仅仅是为了比较大小
  3. 无法按属性校验:如果要检查对象列表中某个属性是否唯一,代码更加冗长
data class User(val id: String, val email: String, val name: String)

val users = listOf(
User("1", "alice@example.com", "Alice"),
User("2", "bob@example.com", "Bob"),
User("3", "charlie@example.com", "Charlie")
)

// 检查邮箱是否唯一——旧写法
val emailsUnique = users.map { it.email }.distinct().size == users.map { it.email }.size
// 这行代码太长了,而且 map 执行了两次

检查所有元素是否相同也不优雅:

val statuses = listOf("active", "active", "active")

// 检查是否全部相同——旧写法
val allSame = statuses.toSet().size == 1
// 或者
val allSame2 = statuses.all { it == statuses.first() }
// 空集合时 first() 会抛异常,需要额外处理

新 API:四个函数,覆盖全部场景​

Kotlin 2.4.20 引入了四个实验性函数:

函数作用返回
allDistinct()集合中所有元素是否互不相同Boolean
allDistinctBy(selector)按指定属性判断元素是否互不相同Boolean
allEqual()集合中所有元素是否完全相同Boolean
allEqualBy(selector)按指定属性判断元素是否完全相同Boolean

这些函数适用于 Collection、Sequence 和 Array,使用结构相等(equals)进行比较。

allDistinct():检查唯一性​

@OptIn(ExperimentalStdlibApi::class)
fun main() {
val ids = listOf("user_001", "user_002", "user_003")
println(ids.allDistinct()) // true

val withDuplicate = listOf("A", "B", "A")
println(withDuplicate.allDistinct()) // false

// 空集合返回 true(vacuous truth)
println(emptyList<String>().allDistinct()) // true

// 单元素集合也返回 true
println(listOf(42).allDistinct()) // true
}

allDistinctBy():按属性检查唯一性​

这是最实用的函数之一——直接按对象的某个属性判断唯一性,无需先 map 再 distinct:

@OptIn(ExperimentalStdlibApi::class)
data class User(val id: String, val email: String, val name: String)

fun main() {
val users = listOf(
User("1", "alice@example.com", "Alice"),
User("2", "bob@example.com", "Bob"),
User("3", "charlie@example.com", "Charlie")
)

// 检查邮箱是否唯一
println(users.allDistinctBy { it.email }) // true

// 检查名字是否唯一
println(users.allDistinctBy { it.name }) // true

// 如果有重复邮箱
val usersWithDup = users + User("4", "alice@example.com", "Alice2")
println(usersWithDup.allDistinctBy { it.email }) // false
}

allEqual() 和 allEqualBy():检查一致性​

当你需要验证一批数据是否"整齐划一"时,这两个函数就派上用场了:

@OptIn(ExperimentalStdlibApi::class)
fun main() {
// 所有配置项相同
val configs = listOf("production", "production", "production")
println(configs.allEqual()) // true

val mixed = listOf("dev", "production", "staging")
println(mixed.allEqual()) // false

// 按属性检查一致性
data class Task(val id: Int, val status: String)
val tasks = listOf(
Task(1, "pending"),
Task(2, "pending"),
Task(3, "pending")
)
println(tasks.allEqualBy { it.status }) // true
}

性能优势:短路求值​

这些新函数不仅仅是语法糖。与 distinct().size == size 不同,它们在发现第一个违规元素时就会立即返回,无需遍历整个集合:

// 旧写法:必须遍历全部元素,创建完整 Set
val hasNoDuplicates = largeList.toSet().size == largeList.size

// 新写法:遇到第一个重复就返回 false
val hasNoDuplicates = largeList.allDistinct()

对于大集合,尤其是重复元素出现在前部的场景,性能差异显著。allEqual() 同理——只要发现一个不同于首元素的值,就立即返回 false。

实际应用场景​

1. 数据校验​

@OptIn(ExperimentalStdlibApi::class)
fun validateOrder(order: Order): Result<Order> {
if (!order.items.allDistinctBy { it.productId }) {
return Result.failure(IllegalArgumentException("订单包含重复商品"))
}
if (order.items.allEqualBy { it.quantity } && order.items.first().quantity == 0) {
return Result.failure(IllegalArgumentException("所有商品数量为零"))
}
return Result.success(order)
}

2. 测试断言​

@OptIn(ExperimentalStdlibApi::class)
@Test
fun `generated IDs should be unique`() {
val ids = (1..1000).map { generateUniqueId() }
assertTrue(ids.allDistinct(), "生成的 ID 存在重复")
}

@Test
fun `all tasks should have same priority after batch update`() {
service.batchUpdatePriority(taskIds, Priority.HIGH)
val tasks = service.getTasks(taskIds)
assertTrue(tasks.allEqualBy { it.priority })
}

3. 问卷/投票系统​

@OptIn(ExperimentalStdlibApi::class)
data class Response(
val participantId: String,
val answer: String,
val responseDate: String
)

fun analyzeSurvey(responses: List<Response>) {
// 检查是否有重复提交
if (!responses.allDistinctBy { it.participantId }) {
println("警告:存在同一参与者的多次提交")
}

// 检查是否所有人答案一致
if (responses.allEqualBy { it.answer }) {
println("所有参与者给出了相同答案")
}

// 检查是否都在同一天提交
if (responses.allEqualBy { it.responseDate }) {
println("所有回答在同一天提交")
}
}

4. 配合 Sequence 处理大数据流​

@OptIn(ExperimentalStdlibApi::class)
fun checkLargeDataset() {
// Sequence 的惰性求值 + allDistinct 的短路求值 = 高效组合
val hasDuplicates = File("data.txt")
.useLines { lines ->
lines.map { it.trim() }
.filter { it.isNotEmpty() }
.allDistinct().not()
}
}

如何启用​

这些函数目前是实验性的,需要添加 opt-in 注解:

// 方式一:文件级注解
@file:OptIn(ExperimentalStdlibApi::class)
package com.example

// 方式二:函数级注解
@OptIn(ExperimentalStdlibApi::class)
fun myFunction() {
val result = myList.allDistinct()
}

或者在 build.gradle.kts 中全局启用:

kotlin {
compilerOptions {
optIn.add("kotlin.ExperimentalStdlibApi")
}
}

与相关 API 的对比​

需求旧写法新写法
元素是否唯一list.distinct().size == list.sizelist.allDistinct()
按属性唯一list.map { it.id }.toSet().size == list.sizelist.allDistinctBy { it.id }
元素是否相同list.toSet().size == 1list.allEqual()
按属性相同list.map { it.status }.toSet().size == 1list.allEqualBy { it.status }
获取重复元素list.groupingBy { it }.eachCount().filter { it.value > 1 }仍需手动实现

注意:新函数只返回 Boolean。如果你需要知道哪些元素重复了,仍然需要 groupingBy 等方案。但对于"是否合法"的校验场景,新 API 已经足够。

小结​

Kotlin 2.4.20 的集合校验函数看似简单,却解决了日常开发中一个高频痛点。它们用语义化的命名替代了需要"翻译"的间接写法,用短路求值替代了无意义的全量遍历,用一个函数调用替代了冗长的链式操作。

如果你的项目已经升级到 Kotlin 2.4.20+,不妨搜索一下代码中的 distinct().size 和 toSet().size,把它们替换成更优雅的新写法。