KMP 实现鸿蒙跨端:Kotlin 去重操作和数据清洁指南
本文介绍了在Kotlin Multiplatform(KMP)中进行数据去重的方法,包括基础去重(distinct)和条件去重(distinctBy),并展示了如何去除集合中的空值(filterNotNull)。这些操作可以编译为JavaScript代码,在OpenHarmony应用中运行。文章提供了完整的Kotlin实现示例及其对应的JavaScript编译结果,以及ArkTS调用代码,演示了如

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中进行去重操作和数据清洁。去重是数据处理中重要的一步,用于移除集合中的重复元素。通过 KMP,这些去重操作可以无缝编译到 JavaScript,在 OpenHarmony 应用中高效运行。
为什么需要学习去重操作?
- 数据清洁:移除重复数据,保证数据质量
- 性能优化:减少数据量,提高处理效率
- 业务逻辑:实现数据去重是很多应用的核心需求
- 代码简洁:使用函数式去重比手写去重逻辑更简洁
- 跨端兼容:去重操作在编译到 JavaScript 时表现出色,完美支持 OpenHarmony
- 代码复用:一份 Kotlin 代码可同时服务多个平台
基础去重
distinct - 去重
移除集合中的重复元素,保持原有顺序。
val numbers = listOf(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5)
val distinct = numbers.distinct()
println(distinct) // [1, 2, 3, 4, 5]
val words = listOf("Apple", "Banana", "Apple", "Cherry", "Banana")
val distinctWords = words.distinct()
println(distinctWords) // [Apple, Banana, Cherry]
代码说明:
这段代码展示了 distinct() 函数的基本用法。第一个示例对数字列表进行去重,移除所有重复的数字,保持原有顺序。第二个示例对字符串列表进行去重,移除重复的单词。distinct() 函数返回一个新的列表,包含所有不重复的元素,并且保持这些元素在原列表中第一次出现的顺序。这种方式简洁高效,是进行基础去重操作的最常用方法。
distinctBy - 按条件去重
按指定条件去重,移除满足条件的重复元素。
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Alice", 25),
Person("Bob", 30),
Person("Alice", 25),
Person("Charlie", 25)
)
// 按名字去重
val distinctByName = people.distinctBy { it.name }
println(distinctByName)
// [Person(Alice, 25), Person(Bob, 30), Person(Charlie, 25)]
// 按年龄去重
val distinctByAge = people.distinctBy { it.age }
println(distinctByAge)
// [Person(Alice, 25), Person(Bob, 30)]
代码说明:
这段代码展示了 distinctBy() 函数的用法,它允许按指定的条件进行去重。首先定义一个 Person 数据类,包含名字和年龄属性。然后创建一个包含多个 Person 对象的列表,其中有重复的人。第一个示例按名字去重,保留每个名字的第一个出现,结果包含三个不同名字的人。第二个示例按年龄去重,保留每个年龄的第一个出现,结果只包含两个不同年龄的人。distinctBy() 根据 Lambda 表达式的结果进行去重,非常灵活。
高级去重
自定义去重逻辑
Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun distinctExample(): String {
val numbers = listOf(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5)
val words = listOf("Apple", "Banana", "Apple", "Cherry", "Banana")
// 去重
val distinctNumbers = numbers.distinct()
val distinctWords = words.distinct()
// 去空
val nullableList = listOf(1, null, 2, null, 3, null, 4)
val withoutNull = nullableList.filterNotNull()
return "原始数字: ${numbers.joinToString(", ")}\n" +
"去重后: ${distinctNumbers.joinToString(", ")}\n" +
"原始单词: ${words.joinToString(", ")}\n" +
"去重后: ${distinctWords.joinToString(", ")}\n" +
"去空后: ${withoutNull.joinToString(", ")}"
}
代码说明:
这是去重操作的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先创建两个包含重复元素的列表(数字和单词)。然后使用 distinct() 函数对这两个列表进行去重。接着创建一个包含 null 元素的列表,使用 filterNotNull() 函数去除所有 null 元素。最后使用 joinToString() 将结果格式化为字符串,展示原始数据和处理后的数据。这个示例展示了如何结合使用去重和去空操作来清洁数据。
编译后的 JavaScript 代码
function distinctExample() {
var numbers = listOf_0([1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5]);
var words = listOf_0(['Apple', 'Banana', 'Apple', 'Cherry', 'Banana']);
// 去重
var distinctNumbers = distinct(numbers);
var distinctWords = distinct(words);
// 去空
var nullableList = listOf_0([1, null, 2, null, 3, null, 4]);
var withoutNull = filterNotNull(nullableList);
return '原始数字: ' + joinToString_0(numbers, ', ') + '\n' +
('去重后: ' + joinToString_0(distinctNumbers, ', ') + '\n') +
('原始单词: ' + joinToString_0(words, ', ') + '\n') +
('去重后: ' + joinToString_0(distinctWords, ', ') + '\n') +
('去空后: ' + joinToString_0(withoutNull, ', '));
}
代码说明:
这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的 distinct() 函数被编译成了 JavaScript 的 distinct() 函数调用。Kotlin 的 filterNotNull() 被编译成了 filterNotNull() 函数调用。Kotlin 的 joinToString() 被编译成了 joinToString_0() 函数调用。虽然编译后的代码看起来不同,但它保留了原始 Kotlin 代码的逻辑,确保了功能的正确性。这个编译过程展示了 KMP 如何将高级的 Kotlin 集合操作转换为可在 JavaScript 环境中运行的代码。
ArkTS 调用代码
import { distinctExample } from './hellokjs';
@Entry
@Component
struct Index {
@State message: string = '加载中...';
@State results: string[] = [];
aboutToAppear(): void {
this.loadResults();
}
loadResults(): void {
try {
// 调用 Kotlin 编译的 JavaScript 函数
const distinctResult = distinctExample();
this.results = [distinctResult];
this.message = '案例已加载';
} catch (error) {
this.message = `错误: ${error}`;
}
}
build() {
Column() {
Text('Kotlin Distinct 去重操作演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 })
Text(this.message)
.fontSize(14)
.fontColor(Color.Gray)
.margin({ bottom: 15 })
Scroll() {
Column() {
ForEach(this.results, (result: string) => {
Text(result)
.fontSize(12)
.fontFamily('monospace')
.padding(12)
.width('100%')
.backgroundColor(Color.White)
.border({ width: 1, color: Color.Gray })
.borderRadius(8)
})
}
.width('100%')
.padding({ left: 15, right: 15 })
}
.layoutWeight(1)
.width('100%')
Button('刷新结果')
.width('80%')
.height(40)
.margin({ bottom: 20 })
.onClick(() => {
this.loadResults();
})
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
}
}
代码说明:
这是 OpenHarmony ArkTS 页面的完整实现,展示了如何集成和调用 Kotlin 编译生成的去重操作示例。首先通过 import 语句从 ./hellokjs 模块导入 distinctExample 函数。页面使用 @Entry 和 @Component 装饰器定义为可入口的组件。定义了两个响应式状态变量:message 显示操作状态,results 存储函数执行结果。aboutToAppear() 生命周期钩子在页面加载时自动调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数获取去重结果,将其存储在 results 数组中,并更新 message 显示加载状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括标题、状态信息、结果展示区域和刷新按钮,使用了 Column、Text、Scroll、Button 等组件构建了一个功能完整的展示界面。
执行流程说明
- Kotlin 源代码:定义
distinctExample()函数,使用 distinct 和 filterNotNull 进行去重和去空 - 编译过程:Gradle 使用 KMP 编译器将 Kotlin 代码编译成 JavaScript
- JavaScript 输出:编译器生成优化的 JavaScript 代码,使用内置函数实现去重逻辑
- ArkTS 调用:在 OpenHarmony 应用中导入并调用编译后的 JavaScript 函数
- 结果展示:在 UI 中显示去重结果
去空操作
filterNotNull - 去空
移除集合中的 null 元素。
val nullableList = listOf(1, null, 2, null, 3, null, 4)
val withoutNull = nullableList.filterNotNull()
println(withoutNull) // [1, 2, 3, 4]
val words = listOf("Apple", null, "Banana", null, "Cherry")
val nonNullWords = words.filterNotNull()
println(nonNullWords) // [Apple, Banana, Cherry]
代码说明:
这段代码展示了 filterNotNull() 函数的基本用法。第一个示例对包含 null 元素的数字列表进行去空,移除所有 null 元素,保留非 null 的数字。第二个示例对包含 null 元素的字符串列表进行去空,移除所有 null 元素,保留非 null 的字符串。filterNotNull() 函数返回一个新的列表,只包含非 null 的元素,并且保持这些元素的原有顺序。这个函数特别有用,因为它同时进行了类型转换,从 List<T?> 转换为 List<T>。
结合去重和去空
val data = listOf(1, null, 2, 2, null, 3, 3, 3, null, 4)
// 先去空再去重
val result = data.filterNotNull().distinct()
println(result) // [1, 2, 3, 4]
// 先去重再去空
val result2 = data.distinct().filterNotNull()
println(result2) // [1, 2, 3, 4]
代码说明:
这段代码展示了如何结合使用 filterNotNull() 和 distinct() 来同时进行去空和去重操作。第一个示例先使用 filterNotNull() 去除所有 null 元素,再使用 distinct() 去除重复元素。第二个示例先使用 distinct() 去除重复元素(包括 null 的重复),再使用 filterNotNull() 去除 null 元素。两种顺序都能得到相同的最终结果 [1, 2, 3, 4],但性能可能略有不同。通常先去空再去重的效率更高,因为去空会减少需要处理的元素数量。
实战案例
案例:去重操作的实际应用
在上面的"高级去重"部分已经展示了完整的三层代码示例(Kotlin、JavaScript、ArkTS)。这个 distinctExample() 案例演示了:
- 基础去重:使用 distinct 去除重复元素
- 去空操作:使用 filterNotNull 去除 null 元素
- 编译过程:展示了 Kotlin 代码如何编译成 JavaScript
- 实际调用:展示了如何在 ArkTS 中调用编译后的函数
扩展应用场景
在实际项目中,可以基于 distinctExample() 的模式进行扩展:
- 用户列表去重:按用户 ID 或邮箱去重
- 商品列表去重:按商品 SKU 去重
- 订单列表去重:按订单号去重
- 标签去重:移除重复的标签
所有这些应用都遵循相同的 Kotlin → JavaScript → ArkTS 的编译和调用流程。
性能优化
1. 选择合适的去重方式
// ✅ 好:使用 distinct()
val result = numbers.distinct()
// ❌ 不好:使用 toSet() 再转回 List
val result = numbers.toSet().toList()
代码说明:
这个示例对比了两种去重方法。第一种方法使用 distinct() 函数,直接返回一个去重后的列表,保持原有顺序。第二种方法先使用 toSet() 转换为 Set(会丢失顺序),再使用 toList() 转换回列表(顺序不确定)。distinct() 方法更简洁、更高效,且保持原有顺序。最佳实践是:当需要去重并保持顺序时,使用 distinct() 而不是 toSet().toList()。
2. 及早去重
// ✅ 好:先去重再处理
val result = numbers
.distinct()
.filter { it > 5 }
.map { it * 2 }
// ❌ 不好:后去重
val result = numbers
.filter { it > 5 }
.map { it * 2 }
.distinct()
代码说明:
这个示例对比了两种处理顺序。第一种方法先使用 distinct() 去重,然后再进行 filter() 和 map() 操作。这样可以减少后续操作需要处理的元素数量,提高性能。第二种方法先进行 filter() 和 map() 操作,最后才去重。这样会处理更多的元素,效率较低。最佳实践是:在链式操作中,应该及早进行去重操作,以减少后续操作的数据量。
3. 使用 distinctBy 代替多次操作
// ✅ 好:使用 distinctBy
val result = people.distinctBy { it.id }
// ❌ 不好:使用 groupBy 再取第一个
val result = people.groupBy { it.id }.values.map { it.first() }
代码说明:
这个示例对比了两种按属性去重的方法。第一种方法使用 distinctBy() 函数,直接按指定属性去重。第二种方法先使用 groupBy() 按属性分组,然后取每组的第一个元素。虽然两种方法都能达到目的,但 distinctBy() 更简洁、更高效。最佳实践是:当需要按属性去重时,使用 distinctBy() 而不是 groupBy().values.map { it.first() }。
4. 结合 filterNotNull 优化
// ✅ 好:链式操作
val result = data
.filterNotNull()
.distinct()
// ❌ 不好:分开操作
val filtered = data.filterNotNull()
val distinct = filtered.distinct()
代码说明:
这个示例对比了两种结合去空和去重的方法。第一种方法使用链式操作,先 filterNotNull() 再 distinct(),代码简洁,中间不产生临时变量。第二种方法将操作分开,先将结果存储在 filtered 变量中,再进行 distinct() 操作,产生了不必要的中间变量。最佳实践是:使用链式操作来组合多个集合操作,这样代码更简洁,也更符合函数式编程的风格。
常见问题
Q1: distinct 和 toSet 有什么区别?
A:
- distinct:保持原有顺序,返回 List
- toSet:不保证顺序,返回 Set
val numbers = listOf(3, 1, 2, 1, 3)
val distinct = numbers.distinct()
println(distinct) // [3, 1, 2]
val set = numbers.toSet()
println(set) // {1, 2, 3} 或其他顺序
代码说明:
这段代码对比了 distinct() 和 toSet() 的区别。distinct() 返回一个列表,保持元素在原列表中第一次出现的顺序,所以结果是 [3, 1, 2]。toSet() 返回一个集合,不保证元素的顺序,所以结果可能是 {1, 2, 3} 或其他顺序。当需要去重并保持顺序时,使用 distinct();当只需要去重且不关心顺序时,可以使用 toSet()。
Q2: distinctBy 如何处理 null 值?
A: distinctBy 会保留第一个 null 值
data class Person(val name: String?, val age: Int)
val people = listOf(
Person(null, 25),
Person(null, 30),
Person("Alice", 25)
)
val result = people.distinctBy { it.name }
println(result)
// [Person(null, 25), Person(Alice, 25)]
代码说明:
这段代码展示了 distinctBy() 如何处理 null 值。当按 name 属性去重时,第一个 Person(null, 25) 被保留,第二个 Person(null, 30) 被移除(因为它的 name 属性也是 null,与第一个重复),Person("Alice", 25) 被保留(因为它的 name 属性是 “Alice”,与前面的都不同)。所以 distinctBy() 将 null 值视为一个有效的去重键,第一个 null 值被保留,后续的 null 值被移除。
Q3: 如何对嵌套集合进行去重?
A: 使用 flatMap 和 distinct
val nestedList = listOf(
listOf(1, 2, 2),
listOf(2, 3, 3),
listOf(3, 4, 4)
)
// 展平并去重
val result = nestedList.flatMap { it }.distinct()
println(result) // [1, 2, 3, 4]
代码说明:
这段代码展示了如何对嵌套集合进行去重。首先使用 flatMap() 将嵌套的列表展平成一个单层列表,flatMap { it } 将每个内层列表的元素逐个提取出来。然后使用 distinct() 对展平后的列表进行去重。结果是 [1, 2, 3, 4],所有重复的元素都被移除了。这种方法可以处理任意深度的嵌套集合。
Q4: 去重操作的时间复杂度是多少?
A: distinct 的时间复杂度为 O(n)
val numbers = listOf(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5)
// 时间复杂度:O(n)
val result = numbers.distinct()
// 时间复杂度:O(n)
val result2 = numbers.distinctBy { it % 2 }
代码说明:
这段代码展示了 distinct() 和 distinctBy() 的时间复杂度。两个函数的时间复杂度都是 O(n),其中 n 是集合的大小。这是因为它们需要遍历集合中的每个元素一次,并使用哈希表或类似的数据结构来追踪已经见过的元素。这种线性时间复杂度使得去重操作非常高效,即使处理大型集合也能快速完成。
Q5: filterNotNull 和 filter { it != null } 有什么区别?
A: filterNotNull 更安全,类型更精确
val nullableList: List<Int?> = listOf(1, null, 2, null, 3)
// ✅ 好:使用 filterNotNull
val result1: List<Int> = nullableList.filterNotNull()
// ❌ 不好:使用 filter
val result2: List<Int?> = nullableList.filter { it != null }
代码说明:
这段代码对比了 filterNotNull() 和 filter { it != null } 的区别。filterNotNull() 不仅移除了 null 元素,还进行了类型转换,将 List<Int?> 转换为 List<Int>,类型更精确。filter { it != null } 虽然也移除了 null 元素,但返回的类型仍然是 List<Int?>,因为编译器无法自动推断出 null 已经被过滤掉。最佳实践是:使用 filterNotNull() 来去除 null 元素,因为它更安全,类型更精确,代码意图也更清晰。
总结
关键要点
- ✅
distinct()是最常用的去重函数 - ✅
distinctBy()用于按属性去重 - ✅
filterNotNull()用于去除 null 元素 - ✅ 及早去重可以提高性能
- ✅ 去重操作保持原有顺序
下一步
- 学习更多高级去重技巧
- 实践复杂的数据去重场景
- 优化去重操作的性能
- 探索自定义去重扩展函数
参考资源
更多推荐




所有评论(0)