在这里插入图片描述

目录

  1. 概述
  2. 基础去重
  3. 高级去重
  4. 去空操作
  5. 实战案例
  6. 性能优化
  7. 常见问题

概述

本文档介绍如何在 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 等组件构建了一个功能完整的展示界面。

执行流程说明
  1. Kotlin 源代码:定义 distinctExample() 函数,使用 distinct 和 filterNotNull 进行去重和去空
  2. 编译过程:Gradle 使用 KMP 编译器将 Kotlin 代码编译成 JavaScript
  3. JavaScript 输出:编译器生成优化的 JavaScript 代码,使用内置函数实现去重逻辑
  4. ArkTS 调用:在 OpenHarmony 应用中导入并调用编译后的 JavaScript 函数
  5. 结果展示:在 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() 案例演示了:

  1. 基础去重:使用 distinct 去除重复元素
  2. 去空操作:使用 filterNotNull 去除 null 元素
  3. 编译过程:展示了 Kotlin 代码如何编译成 JavaScript
  4. 实际调用:展示了如何在 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 元素
  • ✅ 及早去重可以提高性能
  • ✅ 去重操作保持原有顺序

下一步

  1. 学习更多高级去重技巧
  2. 实践复杂的数据去重场景
  3. 优化去重操作的性能
  4. 探索自定义去重扩展函数

参考资源

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐