SwiftUI:使自定义类型符合 Comparable 协议
在编写Swift代码时,我们会想到很多东西。例如,如果我们写4 < 5
,我们期望返回true
– Swift的开发人员(以及LLVM,Swift后面的更大的编译器项目)已经完成了检查该计算是否正确的所有的艰苦工作,因此我们不必为此担心。
但是Swift真正擅长的是使用协议和协议扩展将功能扩展到很多地方。例如,我们知道4 < 5
是正确的,因为我们能够比较两个整数并确定第一个整数是在第二个整数之前还是之后。Swift将功能扩展到整数数组:我们可以比较数组中的所有整数,以确定每个整数应该在另一个整数之前还是之后。然后,Swift使用该结果对数组进行排序。
因此,在Swift中,我们希望这种代码可以正常工作:
struct ContentView: View {
let values = [1, 5, 3, 6, 2, 9].sorted()
var body: some View {
List(values, id: \.self) {
Text(String($0))
}
}
}
我们不需要告诉sorted()
应该如何工作,因为它了解整数数组的工作方式。
现在考虑这样的结构体:
struct User: Identifiable {
let id = UUID()
let firstName: String
let lastName: String
}
我们可以将这些用户组成一个数组,并在这样的List
中使用它们:
struct ContentView: View {
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
]
var body: some View {
List(users) { user in
Text("\(user.lastName), \(user.firstName)")
}
}
}
这样就可以了,因为我们使User
结构体符合Identifiable
协议。
但是,如果我们想按排序顺序显示这些用户呢?如果我们将代码修改为这样,将无法正常工作:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted()
Swift不了解sorted()
在这里的含义,因为它不知道是按名字,姓氏,两者都排序还是其他的条件排序。
之前,我向您展示了如何为sorted()
提供闭包以自行进行排序,并且可以在此处使用相同的代码:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted {
$0.lastName < $1.lastName
}
绝对可行,但是由于两个原因,它不是理想的解决方案。
首先,这是模型数据,这意味着它正在影响我们处理User
结构体的方式。该结构体及其属性是我们的数据模型,在一个开发良好的应用程序中,我们实际上并不想告诉模型在我们的SwiftUI代码中应如何表现。SwiftUI代表了我们的View,即我们的布局,如果我们在其中放置模型代码,那么事情就会变得混乱。
其次,如果我们要在多个位置对用户数组进行排序,会发生什么?您可能会复制或粘贴一次或两次,然后意识到自己只是在为自己创建一个问题:如果最终更改了排序逻辑,以便在姓氏相同的情况下也使用firstName
,则需要搜索遍历所有代码,以确保更新所有闭包。
Swift有更好的解决方案。整数数组获得一个没有参数的简单sorted()
方法,因为Swift知道如何比较两个整数。用编码术语来说,Int
符合Comparable
协议,这意味着它定义了一个接受两个整数的函数,如果第一个应在第二个之前排序,则返回true
。
我们可以使自己的类型符合Comparable
,并且当我们这样做时,我们还将获得没有参数的sorted()
方法。这需要两个步骤:
- 将
Comparable
添加到User
的定义。 - 添加一个名为
<
的方法,该方法需要两个用户,并且如果第一个应在第二个之前排序,则返回true
。
这是代码:
struct User: Identifiable, Comparable {
let id = UUID()
let firstName: String
let lastName: String
static func < (lhs: User, rhs: User) -> Bool {
lhs.lastName < rhs.lastName
}
}
那里没有很多代码,但是还有很多要解决的东西。
首先,是的,该方法仅被称为 <
,它是“小于”运算符。该方法的工作是确定一个用户是否“少于”(在排序意义上)另一个用户,因此我们要向现有的操作符添加功能。这被称为操作符重载,它既是福也是祸。
其次,lhs
和rhs
是“左手边 left-hand side ”和“右手边 right-hand side” 的缩写,它们的使用是因为<
运算符的左侧是一个操作数,右侧是一个操作数。
第三,此方法必须返回一个布尔值,这意味着我们必须确定是否应先将一个对象排序。这里没有“它们相同”的空间——由另一种称为Equatable
的协议处理。
第四,该方法必须标记为静态,这意味着它是直接在User
结构体上调用的,而不是结构体的单个实例。
最后,我们的逻辑非常简单:我们只是将比较传递给我们的一个属性,要求Swift将两个姓氏字符串使用<
。您可以根据需要添加任意数量的逻辑,比较所需的多个属性,但最终需要返回true
或false
。
现在我们的User
结构体符合Comparable
,我们将自动访问sorted()
的无参数版本,这意味着这种代码现在可以使用:
let users = [
User(firstName: "Arnold", lastName: "Rimmer"),
User(firstName: "Kristine", lastName: "Kochanski"),
User(firstName: "David", lastName: "Lister"),
].sorted()
这就解决了我们以前遇到的问题:我们现在将模型功能隔离在结构体本身中,并且不再需要复制和粘贴代码了——我们可以在各处放心的使用sorted()
,因为如果我们更改了算法,那么我们所有的代码都会自动适配。