swift-identified-collections Logo

swift-identified-collections

Swift数据结构库优化可识别元素集合管理

SwiftIdentifiedArray集合数据结构性能优化SwiftUIGithub开源项目

swift-identified-collections库为Swift开发者提供高性能数据结构,用于管理唯一标识元素集合。它解决了普通数组处理可识别元素的性能和稳定性问题。核心组件IdentifiedArray结合了OrderedDictionary的优势,提供了更易用的API。该库适用于SwiftUI应用和Composable Architecture框架开发,提高了代码效率和可靠性。

Swift 标识集合

CI Slack

一个用于以人体工程学和高性能方式处理可识别元素集合的数据结构库。

动机

在为应用程序的状态建模集合元素时,很容易想到使用标准的Array。然而,随着应用程序变得更加复杂,这种方法可能会在多个方面出现问题,包括意外对错误的元素进行修改,甚至导致崩溃。😬

例如,如果你正在使用SwiftUI构建一个"待办事项"应用程序,你可能会将单个待办事项建模为一个可识别的值类型:

struct Todo: Identifiable {
  var description = ""
  let id: UUID
  var isComplete = false
}

然后你会在应用程序的视图模型中将这些待办事项的数组作为已发布的字段持有:

class TodosViewModel: ObservableObject {
  @Published var todos: [Todo] = []
}

视图可以非常简单地渲染这些待办事项的列表,而且由于它们是可识别的,我们甚至可以省略Listid参数:

struct TodosView: View {
  @ObservedObject var viewModel: TodosViewModel
  
  var body: some View {
    List(self.viewModel.todos) { todo in
      ...
    }
  }
}

如果你的部署目标设置为最新版本的SwiftUI,你可能会尝试将绑定传递给列表,以便每一行都可以对其待办事项进行可变访问。这对于简单的情况来说是可行的,但一旦你引入副作用(如API客户端或分析),或者想要编写单元测试,你就必须将这个逻辑推送到视图模型中。这意味着每一行都必须能够将其操作传回视图模型。

你可以通过在视图模型中引入一些端点来实现这一点,比如当一行的完成状态切换时:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at id: Todo.ID) {
    guard let index = self.todos.firstIndex(where: { $0.id == id })
    else { return }
    
    self.todos[index].isComplete.toggle()
    // TODO: 使用API客户端在后端更新待办事项
  }
}

这段代码足够简单,但它可能需要对数组进行完整的遍历才能完成任务。

也许让一行将其索引传回视图模型会更高效,然后它可以通过索引直接修改待办事项。但这会使视图变得更复杂:

List(self.viewModel.todos.enumerated(), id: \.element.id) { index, todo in
  ...
}

这还不算太糟,但目前它甚至无法编译。一个演进提案可能很快会改变这一点,但在此之前,ListForEach必须传递一个RandomAccessCollection,这可能最简单的方法是构造另一个数组:

List(Array(self.viewModel.todos.enumerated()), id: \.element.id) { index, todo in
  ...
}

这可以编译,但我们刚刚将性能问题转移到了视图中:每次评估这个body时,都有可能分配一个全新的数组。

但即使可以将枚举集合直接传递给这些视图,通过索引来识别可变状态的元素也会引入一些其他问题。

虽然我们确实可以大大简化和提高通过索引下标修改元素的任何视图模型方法的性能:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) {
    self.todos[index].isComplete.toggle()
    // TODO: 使用API客户端在后端更新待办事项
  }
}

但是我们添加到这个端点的任何异步工作都必须非常小心,不要在之后使用这个索引。索引不是一个稳定的标识符:待办事项可以随时移动和删除,一个瞬间标识"买生菜"的索引可能下一刻就标识"打电话给妈妈",更糟糕的是,可能是一个完全无效的索引,导致应用程序崩溃!

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) async {
    self.todos[index].isComplete.toggle()
    
    do {
      // ❌ 可能会更新错误的待办事项,或导致崩溃!
      self.todos[index] = try await self.apiClient.updateTodo(self.todos[index]) 
    } catch {
      // 处理错误
    }
  }
}

每当你需要在执行某些异步工作后访问特定的待办事项时,你_必须_遍历数组:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at index: Int) async {
    self.todos[index].isComplete.toggle()
    
    // 1️⃣ 在开始异步工作之前获取待办事项的id引用
    let id = self.todos[index].id
  
    do {
      // 2️⃣ 在后端更新待办事项
      let updatedTodo = try await self.apiClient.updateTodo(self.todos[index])
              
      // 3️⃣ 在异步工作完成后找到待办事项的更新索引
      let updatedIndex = self.todos.firstIndex(where: { $0.id == id })!
      
      // 4️⃣ 更新正确的待办事项
      self.todos[updatedIndex] = updatedTodo
    } catch {
      // 处理错误
    }
  }
}

引入:标识集合

标识集合旨在通过提供以人体工程学和高性能方式处理可识别元素集合的数据结构来解决所有这些问题。

大多数情况下,你可以简单地将Array替换为IdentifiedArray

import IdentifiedCollections

class TodosViewModel: ObservableObject {
  @Published var todos: IdentifiedArrayOf<Todo> = []
  ...
}

然后你可以通过基于id的下标直接修改元素,无需遍历,即使在执行异步工作后也是如此:

class TodosViewModel: ObservableObject {
  ...
  func todoCheckboxToggled(at id: Todo.ID) async {
    self.todos[id: id]?.isComplete.toggle()
    
    do {
      // 1️⃣ 在后端更新待办事项并在todos标识数组中修改它。
      self.todos[id: id] = try await self.apiClient.updateTodo(self.todos[id: id]!)
    } catch {
      // 处理错误
    }

    // 没有步骤 2️⃣ 😆
  }
}

你还可以简单地将标识数组传递给ListForEach等视图,而不会出现任何复杂性:

List(self.viewModel.todos) { todo in
  ...
}

标识数组设计用于与SwiftUI应用程序以及使用Composable Architecture编写的应用程序集成。

设计

IdentifiedArray是Apple的Swift CollectionsOrderedDictionary类型的轻量级包装器。它具有许多相同的性能特征和设计考虑,但更适合解决在应用程序状态中保存_可识别_元素集合的问题。

IdentifiedArray不会暴露任何可能导致破坏不变性的OrderedDictionary细节。例如,OrderedDictionary<ID, Identifiable>可以自由地保存其标识符与其键不匹配的值,或多个值可能具有相同的id,而IdentifiedArray不允许这些情况发生。

OrderedSet不同,IdentifiedArray不要求其Element类型符合Hashable协议,这可能难以或不可能实现,并引入了关于哈希质量等问题。

IdentifiedArray甚至不要求其Element符合Identifiable。就像SwiftUI的ListForEach视图接受一个id键路径来获取元素的标识符一样,IdentifiedArray可以使用键路径构造:

var numbers = IdentifiedArray(id: \Int.self)

性能

IdentifiedArray设计用于匹配OrderedDictionary的性能特征。它已经使用Swift Collections Benchmark进行了基准测试:

社区

如果你想讨论这个库或有关如何使用它解决特定问题的问题,有几个地方可以与其他Point-Free爱好者讨论:

文档

Identified Collections的最新API文档可在这里获得。

翻译

有兴趣了解更多?

这些概念(以及更多)在Point-Free中得到了深入探讨,这是一个由Brandon WilliamsStephen Celis主持的探索函数式编程和Swift的视频系列。

Composable Architecture中使用IdentifiedArray的内容在以下Point-Free集中进行了探讨:

视频海报图片

许可证

所有模块都在MIT许可证下发布。有关详细信息,请参阅LICENSE