在这里插入图片描述

目录

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

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中进行分组操作和数据组织。分组操作是将集合中的元素按照某种条件分组的过程。通过 KMP,这些分组操作可以无缝编译到 JavaScript,在 OpenHarmony 应用中高效运行。

为什么需要学习分组操作?

  • 数据组织:按条件将数据分组,便于后续处理
  • 数据分析:对分组数据进行统计和分析
  • 业务逻辑:实现复杂的数据分类和组织需求
  • 代码简洁:使用函数式分组比循环更清晰
  • 跨端兼容:分组操作在编译到 JavaScript 时表现出色,完美支持 OpenHarmony
  • 代码复用:一份 Kotlin 代码可同时服务多个平台

基础分组

groupBy - 按条件分组

将集合中的元素按照条件分组,返回一个 Map。

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 按奇偶性分组
val grouped = numbers.groupBy { if (it % 2 == 0) "偶数" else "奇数" }
println(grouped)
// {偶数=[2, 4, 6, 8, 10], 奇数=[1, 3, 5, 7, 9]}

// 按模 3 分组
val groupedBy3 = numbers.groupBy { it % 3 }
println(groupedBy3)
// {1=[1, 4, 7, 10], 2=[2, 5, 8], 0=[3, 6, 9]}

代码说明:

这段代码展示了 groupBy() 函数的基本用法。第一个示例使用 Lambda 表达式判断数字的奇偶性,将偶数和奇数分别分组到不同的键下。groupBy() 返回一个 Map,其中键是分组条件的结果,值是满足该条件的元素列表。第二个示例按模 3 的余数分组,将数字分成三组。这种方式可以处理任意的分组条件,非常灵活。groupBy() 返回的是 LinkedHashMap,保持插入顺序,便于后续处理。

groupingBy - 分组后聚合

使用 groupingBy 可以对分组后的数据进行聚合操作。

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 按奇偶性分组,计算每组的大小
val counts = numbers.groupingBy { if (it % 2 == 0) "偶数" else "奇数" }
    .eachCount()
println(counts)
// {偶数=5, 奇数=5}

// 按奇偶性分组,计算每组的总和
val sums = numbers.groupingBy { if (it % 2 == 0) "偶数" else "奇数" }
    .fold(0) { acc, value -> acc + value }
println(sums)
// {偶数=30, 奇数=25}

代码说明:

这段代码展示了 groupingBy() 函数的用法,它用于分组后进行聚合操作。与 groupBy() 不同,groupingBy() 返回一个 Grouping 对象,可以链式调用聚合函数。第一个示例使用 eachCount() 计算每组的元素个数。第二个示例使用 fold() 计算每组的总和,fold() 接收一个初始值和一个聚合函数,将集合中的元素逐个聚合。这种方式比先 groupBy()mapValues() 更高效,因为只需要一次遍历。


高级分组

按对象属性分组

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun groupByExample(): String {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
    // 按奇偶性分组
    val grouped = numbers.groupBy { if (it % 2 == 0) "偶数" else "奇数" }
    
    // 构建结果字符串
    val result = mutableListOf<String>()
    grouped.forEach { (key, values) ->
        result.add("$key: ${values.joinToString(", ")}")
    }
    
    return "按奇偶分组:\n${result.joinToString("\n")}"
}

代码说明:

这是分组操作的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先创建一个数字列表,然后使用 groupBy() 按奇偶性进行分组。接着使用 forEach() 遍历分组结果,对每个分组(键值对)进行处理。使用 joinToString() 将每组的数字转换为逗号分隔的字符串。最后将所有分组结果格式化为多行字符串返回。这个示例展示了如何将分组结果转换为用户友好的格式。

编译后的 JavaScript 代码
function groupByExample() {
  var numbers = listOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
  
  // Inline function 'kotlin.collections.groupBy' call
  // Inline function 'kotlin.collections.groupByTo' call
  var destination = LinkedHashMap_init_$Create$();
  var _iterator__ex2g4s = numbers.b();
  while (_iterator__ex2g4s.c()) {
    var element = _iterator__ex2g4s.d();
    // Inline function 'groupByExample.<anonymous>' call
    var key = (element % 2 | 0) === 0 ? '偶数' : '奇数';
    // Inline function 'kotlin.collections.getOrPut' call
    var value = destination.m(key);
    var tmp;
    if (value == null) {
      // Inline function 'kotlin.collections.groupByTo.<anonymous>' call
      var answer = ArrayList_init_$Create$();
      destination.r1(key, answer);
      tmp = answer;
    } else {
      tmp = value;
    }
    var list = tmp;
    list.c1(element);
  }
  var grouped = destination;
  
  // Inline function 'kotlin.collections.mutableListOf' call
  var result = ArrayList_init_$Create$();
  // Inline function 'kotlin.collections.forEach' call
  // Inline function 'kotlin.collections.iterator' call
  var _iterator__ex2g4s_0 = grouped.n().b();
  while (_iterator__ex2g4s_0.c()) {
    var element_0 = _iterator__ex2g4s_0.d();
    // Inline function 'groupByExample.<anonymous>' call
    // Inline function 'kotlin.collections.component1' call
    var key_0 = element_0.j();
    // Inline function 'kotlin.collections.component2' call
    var values = element_0.k();
    result.c1(key_0 + ': ' + joinToString_0(values, ', '));
  }
  return '按奇偶分组:\n' + joinToString_0(result, '\n');
}

代码说明:

这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的 groupBy() 函数被编译成了 JavaScript 的 while 循环。首先创建一个 LinkedHashMap 来存储分组结果。然后遍历输入列表,对每个元素计算分组键(奇偶性判断)。使用 getOrPut() 逻辑获取或创建对应键的列表。将元素添加到相应的列表中。最后遍历分组结果,构建格式化的字符串。虽然编译后的代码看起来复杂,但它保留了原始 Kotlin 代码的逻辑,确保了功能的正确性。

ArkTS 调用代码
import { groupByExample } from './hellokjs';

@Entry
@Component
struct Index {
  @State message: string = '加载中...';
  @State results: string[] = [];

  aboutToAppear(): void {
    this.loadResults();
  }

  loadResults(): void {
    try {
      // 调用 Kotlin 编译的 JavaScript 函数
      const groupByResult = groupByExample();
      this.results = [groupByResult];
      this.message = '案例已加载';
    } catch (error) {
      this.message = `错误: ${error}`;
    }
  }

  build() {
    Column() {
      Text('Kotlin GroupBy 分组操作演示')
        .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 模块导入 groupByExample 函数。页面使用 @Entry@Component 装饰器定义为可入口的组件。定义了两个响应式状态变量:message 显示操作状态,results 存储函数执行结果。aboutToAppear() 生命周期钩子在页面加载时自动调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数获取分组结果,将其存储在 results 数组中,并更新 message 显示加载状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括标题、状态信息、结果展示区域和刷新按钮,使用了 Column、Text、Scroll、Button 等组件构建了一个功能完整的展示界面。

执行流程说明
  1. Kotlin 源代码:定义 groupByExample() 函数,使用 groupBy 进行分组
  2. 编译过程:Gradle 使用 KMP 编译器将 Kotlin 代码编译成 JavaScript
  3. JavaScript 输出:编译器生成优化的 JavaScript 代码,使用 LinkedHashMap 实现分组逻辑
  4. ArkTS 调用:在 OpenHarmony 应用中导入并调用编译后的 JavaScript 函数
  5. 结果展示:在 UI 中显示分组结果

分割操作

partition - 分割成两部分

将集合分割成满足条件和不满足条件的两部分。

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 分割成偶数和奇数
val (evens, odds) = numbers.partition { it % 2 == 0 }
println("偶数: $evens")  // [2, 4, 6, 8, 10]
println("奇数: $odds")   // [1, 3, 5, 7, 9]

// 分割成大于 5 和小于等于 5
val (large, small) = numbers.partition { it > 5 }
println("大于5: $large")  // [6, 7, 8, 9, 10]
println("≤5: $small")     // [1, 2, 3, 4, 5]

代码说明:

这段代码展示了 partition() 函数的用法,它将集合分割成两部分。partition() 接收一个 Lambda 表达式作为条件,返回一个 Pair<List<T>, List<T>>,第一个列表包含满足条件的元素,第二个列表包含不满足条件的元素。第一个示例按奇偶性分割,第二个示例按大小分割。使用解构赋值 val (evens, odds) = 可以方便地获取两个分割后的列表。partition() 只需要一次遍历,比先 filter() 再取反 filter() 更高效。

多条件分割

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 分割成三部分
val small = numbers.filter { it < 4 }
val medium = numbers.filter { it in 4..7 }
val large = numbers.filter { it > 7 }

println("小于4: $small")     // [1, 2, 3]
println("4-7: $medium")      // [4, 5, 6, 7]
println("大于7: $large")     // [8, 9, 10]

代码说明:

这段代码展示了如何使用多个 filter() 调用将集合分割成多个部分。虽然 partition() 只能分成两部分,但可以使用多个 filter() 调用实现多条件分割。第一个 filter() 筛选小于 4 的数字,第二个筛选在 4 到 7 之间的数字,第三个筛选大于 7 的数字。这种方式虽然需要多次遍历,但代码清晰易读。对于需要分成多个部分的场景,这是一个简单有效的解决方案。


实战案例

案例:分组操作的实际应用

在上面的"高级分组"部分已经展示了完整的三层代码示例(Kotlin、JavaScript、ArkTS)。这个 groupByExample() 案例演示了:

  1. 基础分组:使用 groupBy 按奇偶性分组
  2. 编译过程:展示了 Kotlin 代码如何编译成 JavaScript
  3. 实际调用:展示了如何在 ArkTS 中调用编译后的函数
扩展应用场景

在实际项目中,可以基于 groupByExample() 的模式进行扩展:

  • 用户分类:按年龄、地区、等级等条件分组用户
  • 订单分类:按状态、日期、金额等条件分组订单
  • 商品分类:按类别、品牌、价格等条件分组商品
  • 日志分析:按日志级别、模块、时间等条件分组日志

所有这些应用都遵循相同的 Kotlin → JavaScript → ArkTS 的编译和调用流程。


性能优化

1. 选择合适的分组方式

// ✅ 好:使用 groupBy
val grouped = numbers.groupBy { it % 2 == 0 }

// ❌ 不好:使用 filter 多次遍历
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filter { it % 2 != 0 }

代码说明:

这个示例对比了两种分组方法。第一种方法使用 groupBy(),只需要一次遍历就能完成分组,返回一个 Map。第二种方法使用两个 filter() 调用,需要遍历集合两次。对于大数据集,groupBy() 的性能更好。最佳实践是:当需要按条件分组时,优先使用 groupBy() 而不是多个 filter() 调用。

2. 使用 groupingBy 进行聚合

// ✅ 好:一次遍历完成分组和聚合
val sums = numbers.groupingBy { it % 2 == 0 }
    .fold(0) { acc, value -> acc + value }

// ❌ 不好:先分组再聚合
val grouped = numbers.groupBy { it % 2 == 0 }
val sums = grouped.mapValues { (_, values) -> values.sum() }

代码说明:

这个示例对比了两种分组后聚合的方法。第一种方法使用 groupingBy() 链式调用 fold(),在一次遍历中完成分组和聚合。第二种方法先使用 groupBy() 分组,再使用 mapValues() 对每组进行聚合,需要两次遍历。对于需要分组后进行聚合的场景,groupingBy() 更高效。最佳实践是:使用 groupingBy() 进行分组后的聚合操作。

3. 使用 partition 代替多次 filter

// ✅ 好:使用 partition
val (evens, odds) = numbers.partition { it % 2 == 0 }

// ❌ 不好:多次 filter
val evens = numbers.filter { it % 2 == 0 }
val odds = numbers.filter { it % 2 != 0 }

代码说明:

这个示例对比了两种分割集合的方法。第一种方法使用 partition(),只需要一次遍历就能同时得到满足条件和不满足条件的两个列表。第二种方法使用两个 filter() 调用,需要遍历集合两次。对于需要分成两部分的场景,partition() 更高效。最佳实践是:当需要分割成两部分时,使用 partition() 而不是多个 filter() 调用。


常见问题

Q1: groupBy 和 partition 有什么区别?

A:

  • groupBy:按条件分组,返回 Map,可以有多个分组
  • partition:分割成两部分,返回 Pair,只能分成满足和不满足两部分
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// groupBy:可以有多个分组
val grouped = numbers.groupBy { it % 3 }
// {1=[1, 4, 7, 10], 2=[2, 5, 8], 0=[3, 6, 9]}

// partition:只能分成两部分
val (evens, odds) = numbers.partition { it % 2 == 0 }
// [2, 4, 6, 8, 10], [1, 3, 5, 7, 9]

代码说明:

这段代码对比了 groupBy()partition() 的区别。groupBy() 可以按任意条件分组,返回一个 Map,其中键是分组条件的结果,可以有多个分组。partition() 只能分成两部分,返回一个 Pair,第一个元素是满足条件的列表,第二个元素是不满足条件的列表。选择哪一个取决于需求:如果需要多个分组,使用 groupBy();如果只需要分成两部分,使用 partition() 更高效。

Q2: 如何对分组后的数据进行聚合?

A: 使用 groupingBy 或 groupBy 后 mapValues

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 方式 1:使用 groupingBy
val sums = numbers.groupingBy { it % 2 == 0 }
    .fold(0) { acc, value -> acc + value }

// 方式 2:使用 groupBy 后 mapValues
val sums2 = numbers.groupBy { it % 2 == 0 }
    .mapValues { (_, values) -> values.sum() }

代码说明:

这段代码展示了两种对分组后的数据进行聚合的方法。第一种方法使用 groupingBy() 链式调用 fold(),在一次遍历中完成分组和聚合。第二种方法先使用 groupBy() 分组得到 Map,再使用 mapValues() 对每组的值进行聚合。两种方法都能得到相同的结果,但第一种方法更高效,因为只需要一次遍历。

Q3: 如何按多个条件分组?

A: 使用 groupBy 嵌套或创建复合键

data class Person(val name: String, val age: Int, val city: String)

val people = listOf(
    Person("Alice", 25, "北京"),
    Person("Bob", 30, "上海"),
    Person("Charlie", 25, "北京")
)

// 方式 1:按年龄分组
val byAge = people.groupBy { it.age }

// 方式 2:按城市分组
val byCity = people.groupBy { it.city }

// 方式 3:按年龄和城市分组(复合键)
val byAgeAndCity = people.groupBy { it.age to it.city }

代码说明:

这段代码展示了如何按多个条件分组。第一种方法按单个属性(年龄)分组。第二种方法按另一个属性(城市)分组。第三种方法使用 Pair 创建复合键,按年龄和城市同时分组。使用复合键时,只有年龄和城市都相同的人才会被分到同一组。这种方式可以处理任意复杂的分组条件。

Q4: groupBy 返回的 Map 的顺序是什么?

A: 返回的是 LinkedHashMap,保持插入顺序

val numbers = listOf(1, 2, 3, 4, 5)
val grouped = numbers.groupBy { it % 2 == 0 }

// 遍历顺序与第一次出现的顺序相同
grouped.forEach { (key, values) ->
    println("$key: $values")
}
// false: [1, 3, 5]
// true: [2, 4]

代码说明:

这段代码展示了 groupBy() 返回的 Map 的顺序特性。groupBy() 返回的是 LinkedHashMap,它保持键的插入顺序。遍历顺序与分组键第一次出现的顺序相同。在这个例子中,false(奇数)首先出现,所以在遍历时 false 会先被输出,然后是 true(偶数)。这种可预测的顺序使得分组结果更容易理解和处理。

Q5: 如何处理空集合的分组?

A: 空集合分组后返回空 Map

val empty = emptyList<Int>()

val grouped = empty.groupBy { it % 2 == 0 }
println(grouped)  // {}

val (evens, odds) = empty.partition { it % 2 == 0 }
println(evens)    // []
println(odds)     // []

代码说明:

这段代码展示了如何处理空集合的分组。对空集合使用 groupBy() 返回一个空 Map {}。对空集合使用 partition() 返回两个空列表 []。这种行为是安全的,不会抛出异常。在实际应用中,需要考虑空集合的情况,确保代码能够正确处理。


总结

关键要点

  • groupBy 是最常用的分组操作
  • partition 用于分割成两部分
  • groupingBy 用于分组后聚合
  • ✅ 选择合适的分组方式可以提高性能
  • ✅ 分组后可以进行进一步的数据处理

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐