-
- 1.1. 什么是多重标准和属性排序?
- 1.2. 对两个字段上的对象数组进行排序
- 1.3. 对三个字段以上的对象数组进行排序
- 1.4. The problem
- 1.5. 对N个字段上的对象数组进行排序
-
- 2.1. 函数的灵活性:使用OC中的NSSortDescriptor
- 2.2. 函数作为依据
1. 在Swift中如何排序的多个属性?
如果您按照一个条件或单个属性进行排序,那么排序是很容易的。Swift已经有了这样的功能。
下面是一个对int型数组进行排序的例子。
let numbers = [3, 5, 6, 1, 8, 2]
let sortedNumbers = numbers.sorted { (lhs, rhs) in
return lhs < rhs
}
// [1, 2, 3, 5, 6, 8]
但是有时您需要根据多个条件或属性进行排序。为了演示这一点,让我们以创建一个结构为例。
这里我们有一个简单的***BlogPost***结构,带有文章的标题和两个统计数据点、页面视图和会话持续时间。
struct BlogPost {
let title: String
let pageView: Int
let sessionDuration: Double
}
这是一个样本数据。
extension BlogPost {
static var examples: [BlogPost] = [
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
]
}
如果你想看看哪些帖子表现得很好,你可以从按页面视图排序开始。但正如你所看到的,许多帖子并没有那么受欢迎,也有相同的页面浏览量。在这种情况下,您需要另一个标准或属性来进行进一步的排序。
这种多属性排序是我们将在本文中讨论的。他们有各种各样的方法来解决这个问题。我将展示最基本的方法,没有任何高级的概念。一旦你理解了基本原理,你就可以随心所欲地进行改进。
1.1. 什么是多重标准和属性排序?
多重条件排序是指我们比较第一个条件,并且只有当第一个条件相等时,我们才会转到下一个。我们一直这样做,直到找到一个不相等的标准。
伪代码看起来像这样:
let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] { // <1>
if lhsCriteria == rhsCriteria { // <2>
continue
}
return lhsCriteria < rhsCriteria // <3>
}
}
-
我们循环遍历条件列表,从最重要的条件(第一个)开始。
-
如果顺序标准是相等的,我们不能决定顺序,我们移动到下一个标准。
-
如果我们可以从条件中决定两个对象之间的顺序,我们就停止并返回结果。
如果您很难理解伪代码,不要担心。我不是伪代码的专业写手。下面的例子应该更清楚。
1.2. 对两个字段上的对象数组进行排序
我们将使用上面提到的相同场景。我们希望根据性能对BlogPost进行排序。我们的性能由页面视图(pageView)的数量决定,如果博客文章有相同的页面视图,我们使用会话持续时间(sessionDuration)。
下面是我们在前面的示例中使用的一个BlogPost结构和示例数据。
struct BlogPost {
let title: String
let pageView: Int
let sessionDuration: Double
}
extension BlogPost {
static var examples: [BlogPost] = [
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
]
}
我们衡量性能的方式可以转化为这种代码。
let popularPosts = BlogPost.examples.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView { // <1>
return lhs.sessionDuration > rhs.sessionDuration
}
return lhs.pageView > rhs.pageView // <2>
}
-
如果博客文章有相同的页面视图,我们使用会话持续时间。
-
如果页面浏览量不相等,我们可以根据页面浏览量来决定顺序。(我们按降序排序)
这是我们的结果。
[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]
1.3. 对三个字段以上的对象数组进行排序
如您所见,按照两个标准执行排序非常容易。让我们在方程中增加更多的标准。如果博客文章具有相同的性能,我们将按名称对其进行排序。
让我们在示例中添加更多的博客文章。
extension BlogPost {
static var examples2: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
}
两个和三个标准没有区别。我们可以用和之前一样的逻辑。
let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView {
if lhs.sessionDuration == rhs.sessionDuration { // <1>
return lhs.title < rhs.title
}
return lhs.sessionDuration > rhs.sessionDuration
}
return lhs.pageView > rhs.pageView
}
- 我们添加了另一个if来检查博客文章是否有相同的会话持续时间,如果它们有相同的页面访问量和会话持续时间,就按标题进行排序。
Results:
[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]
1.4. The problem
我们可以用同样的逻辑来处理2个和3个标准。这里唯一的问题是,条件越多,您需要的if-else嵌套越多。
这里有一个可能导致末日金字塔的多重标准的例子。
let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView {
if lhs.sessionDuration == rhs.sessionDuration {
if lhs.nextCriteria == rhs.nextCriteria {
if lhs.nextCriteria == rhs.nextCriteria {
....
}
...
}
...
}
return lhs.sessionDuration > rhs.sessionDuration
}
return lhs.pageView > rhs.pageView
}
1.5. 对N个字段上的对象数组进行排序
为了解决末日金字塔,让我们回到之前看到的伪代码。
let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
if lhsCriteria == rhsCriteria {
continue
}
return lhsCriteria < rhsCriteria
}
}
上面的代码并不是解决这类问题的唯一方法,但键应该是类似的。关键是我们将条件打包到一个集合中,以便进行循环。
extension BlogPost {
static var examples2: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
}
typealias AreInIncreasingOrder = (BlogPost, BlogPost) -> Bool // <1>
let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [ // <2>
{ $0.pageView > $1.pageView },
{ $0.sessionDuration > $1.sessionDuration},
{ $0.title < $1.title }
]
for predicate in predicates { // <3>
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { // <4>
continue // <5>
}
return predicate(lhs, rhs) // <5>
}
return false
}
-
我声明了一个别名AreInIncreasingOrder,它匹配排序闭包。这提高了声明谓词集合时的可读性。
-
我们声明了一组谓词。
-
我们循环谓词。
-
这里有一个棘手的部分,我们想检查条件是否可以决定博客帖子的顺序。但是AreInIncreasingOrder返回一个布尔值。我们如何检查订单是否相同?在回答这个问题之前,让我们先来看看AreInIncreasingOrder的定义。
AreInIncreasingOrder是一个谓词,如果第一个参数能决定顺序时返回true; 否则返回false。所以,只有当两个参数不是递增顺序时,两个参数才相等。
这意味着无论我们的参数顺序如何,谓词都必须是 false。换言之 lhs.pageView < rhs.pageView 和 rhs.pageView < lhs.pageView必须等于false才能决定顺序相等。这就是我们 !predicate(lhs, rhs) && !predicate(rhs, lhs) 这句代码的意思。
-
如果顺序是相等的,我们继续下一个谓词。
-
如果顺序不相等,我们可以使用谓词来决定顺序。
Results:
[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]
2. 排序进阶篇
2.1. 函数的灵活性:使用OC中的NSSortDescriptor
我们从定义一个 Person 类型开始。因为我们想要展示 Objective-C 强大的运行时的工作方式,所以我们将它定义为 NSObject 的子类 (在纯 Swift 中,使用结构体会是更好的选择)。我们将这个类标记为 @objcMembers,这样它的所有成员都将在 Objective-C 中可见
@objcMembers
final class Person: NSObject {
let first: String
let last: String
let yearOfBirth: Int
init(first: String, last: String, yearOfBirth: Int) {
self.first = first
self.last = last
self.yearOfBirth = yearOfBirth
// super.init() 在这里被隐式调用
}
}
接下来我们定义一个数组,其中包含了不同名字和出生年份的人:
let people = [
Person(first: "Emily", last: "Young", yearOfBirth: 2002),
Person(first: "David", last: "Gray", yearOfBirth: 1991),
Person(first: "Robert", last: "Barnes", yearOfBirth: 1985),
Person(first: "Ava", last: "Barnes", yearOfBirth: 2000),
Person(first: "Joanne", last: "Miller", yearOfBirth: 1994),
Person(first: "Ava", last: "Barnes", yearOfBirth: 1998),
]
我们想要对这个数组进行排序,规则是先按照姓排序,再按照名排序,最后是出生年份。排序应该遵照用户的区域设置。我们可以使用 NSSortDescriptor 对象来描述如何排序对象,通过它可以表达出各个排序标准 (使用 localizedStandardCompare 来进行遵循区域设置的排序)
let lastDescriptor = NSSortDescriptor(key: #keyPath(Person.last),
ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: #keyPath(Person.first),
ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: #keyPath(Person.yearOfBirth),
ascending: true)
要对数组进行排序,我们使用 NSArray 的 sortedArray(using:) 方法。这个方法可以接受一系列排序描述符。为了确定两个元素的顺序,它会先使用第一个描述符,并检查其结果。如果两个元素在第一个描述符下相同,那么它将使用第二个描述符,以此类推:
let descriptors = [lastDescriptor, firstDescriptor, yearDescriptor]
(people as NSArray).sortedArray(using: descriptors)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)]
*/
排序描述符用到了 Objective-C 的两个运行时特性:首先,key 是 Objective-C 的键路径,它其实是一个包含属性名字的链表。不要把它和 Swift 4 引入的原生的 (强类型的) 键路径搞混。我们会在稍后再对它进行更多讨论。
其次是键值编程 (key-value-coding),它可以在运行时通过键查找一个对象上的对应值。selector 参数接受一个 selector (实际上也是一个用来描述方法名字的字符串),在运行时,这个 selector 将被用来查找比较函数,当对两个对象进行比较时,这个函数将使用指定键对应的值进行比较。
这是运行时编程的一个很酷的用例,排序描述符的数组可以在运行时构建,这一点在实现比如用户点击某一列时按照该列进行排序这种需求时会特别有用。
我们要怎么用 Swift 的 sort 来复制这个功能呢?要复制这个排序的部分功能是很简单的,比如,你想要使用 localizedStandardCompare 来排序一个数组的话:
var strings = ["Hello", "hallo", "Hallo", "hello"]
strings.sort { $0.localizedStandardCompare($1) == .orderedAscending}
strings // ["hallo", "Hallo", "hello", "Hello"]
如果只是想用对象的某一个属性进行排序的话,也非常简单:
people.sorted { $0.yearOfBirth < $1.yearOfBirth }
/*
[Robert Barnes (1985), David Gray (1991), Joanne Miller (1994),
Ava Barnes (1998), Ava Barnes (2000), Emily Young (2002)]
*/
不过,当你要把可选值属性与像是 localizedStandardCompare 这样的方法结合起来使用的话,这条路就有点儿走不通了。代码会迅速变得丑陋不堪。例如,我们想用在可选值中定义的 fileExtension 属性来对一个包含文件名的数组进行排序:
var files = ["one", "file.h", "file.c", "test.h"]
files.sort { l, r in r.fileExtension.flatMap {
l.fileExtension?.localizedStandardCompare($0)
} == .orderedAscending }
files // ["one", "file.c", "file.h", "test.h"]
这真的很丑。稍后我们会让可选值的排序稍微容易一些。不过就目前而言,我们甚至还没尝试对多个属性进行排序。要同时排序姓和名,我们可以用标准库的 lexicographicallyPrecedes 方法来进行实现。这个方法接受两个序列,并对它们执行一个电话簿方式的比较,也就是说,这个比较将顺次从两个序列中各取一个元素来进行比较,直到发现不相等的元素。所以,我们可以用姓和名构建两个数组,然后使用 lexicographicallyPrecedes 来比较它们。我们还需要一个函数来执行这个比较,这里我们把使用了 localizedStandardCompare 的比较代码放到这个函数中:
people.sorted { p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right) {
$0.localizedStandardCompare($1) == .orderedAscending
}
}
/*
[Ava Barnes (2000), Ava Barnes (1998), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)]
*/
至此,我们用了差不多相同的行数重新实现了原来的那个排序方法。不过还有很大的改进空间:在每次比较的时候都构建一个数组是非常没有效率的,比较操作也是被写死的,通过这种方法我们将无法实现对 yearOfBirth 的排序。
2.2. 函数作为依据
我们不会选择去写一个更复杂的函数来进行排序,先回头看看现状。排序描述符的方式要清晰不少,但是它用到了运行时编程。我们写的函数没有使用运行时编程,不过它们不太容易写出来或者读懂。
除了把排序信息存储在类里,我们还可以定义一个描述对象顺序的函数。其中,最简单的一种实现就是接受两个对象作为参数,并在它们顺序正确的时候,返回 true。这个函数的类型正是标准库中 sort(by:) 和 sorted(by:) 的参数类型。接下来,让我们先定义一个泛型别名来表达这种函数形式的排序描述符:
/// 一个排序断言,当第一个值应当排在第二个值之前时,返回 `true`
typealias SortDescriptor<Root> = (Root, Root) -> Bool
现在,就可以用这个别名定义比较 Person 对象的排序描述符了。它可以比较出生年份,也可以比较姓的字符串:
let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
let sortByLastName: SortDescriptor<Person> = {
$0.last.localizedStandardCompare($1.last) == .orderedAscending
}
除了手写这些排序描述符外,我们也可以创建一个函数来生成它们。将相同的属性写两次并不太好,比如在 sortByLastName 中,我们很容易就会不小心弄成 $0.last 和 $1.first 进行比较。而且写排序描述符本身也挺无聊的:想要通过名来排序的时候,很可能你就把姓排序的 sortByLastName 复制粘贴一下,然后再进行修改。
为了避免复制粘贴,我们可以定义一个函数,它和 NSSortDescriptor 大体相似,但不涉及运行时编程。这个函数的第一个参数是一个名为 key 的函数,此函数接受一个正在排序的数组的元素,并返回这个排序描述符所处理的属性的值。然后,我们使用第二个参数 areInIncreasingOrder 比较 key 返回的结果。最后,用 SortDescriptor 把这两个参数包装一下,就是要返回的排序描述符了:
/// `key` 函数,根据输入的参数返回要进行比较的元素
/// `by` 进行比较的断言
/// 通过用 `by` 比较 `key` 返回值的方式构建 `SortDescriptor` 函数
func sortDescriptor<Root, Value>( key: @escaping (Root) -> Value, by areInIncreasingOrder: @escaping (Value, Value) -> Bool)
-> SortDescriptor<Root>
{
return { areInIncreasingOrder(key($0), key($1)) }
}
key 函数描述了如何深入一个 Root 类型的元素,并提取出一个和特定排序步骤相关的 Value 类型的值。因为借鉴了泛型参数名字 Root 和 Value,所以它和 Swift 4 引入的 Swift 原生键路径有很多相同之处。我们会在下面讨论怎么用 Swift 的键路径重写这个方法。
有了这个函数,我们就可以用另外一种方式来定义 sortByYear 了:
let sortByYearAlt: SortDescriptor<Person> =
sortDescriptor(key: { $0.yearOfBirth }, by: <)
people.sorted(by: sortByYearAlt)
/*
[Robert Barnes (1985), David Gray (1991), Joanne Miller (1994),
Ava Barnes (1998), Ava Barnes (2000), Emily Young (2002)]
*/
甚至,我们还可以为所有实现了 Comparable 的类型定义一个重载版本:
func sortDescriptor<Root, Value>(key: @escaping (Root) -> Value)
-> SortDescriptor<Root> where Value: Comparable
{
return { key($0) < key($1) }
}
let sortByYearAlt2: SortDescriptor<Person> =
sortDescriptor(key: { $0.yearOfBirth })
这两个 sortDescriptor 都使用了返回布尔值的排序函数,因为这是标准库中对于比较断言的约定。但另一方面,Foundation 中像是 localizedStandardCompare 这样的 API,返回的却是一个包含 (升序,降序,相等) 三种值的 ComparisonResult。给 sortDescriptor 增加这种支持也很简单:
func sortDescriptor<Root, Value>(
key: @escaping (Root) -> Value,
ascending: Bool = true,
by comparator: @escaping (Value) -> (Value) -> ComparisonResult)
-> SortDescriptor<Root>
{
return { lhs, rhs in
let order: ComparisonResult = ascending
? .orderedAscending
: .orderedDescending
return comparator(key(lhs))(key(rhs)) == order
}
}
这样,我们就可以用简短清晰得多的方式来写 sortByFirstName 了:
let sortByFirstName: SortDescriptor<Person> =
sortDescriptor(key: { $0.first }, by: String.localizedStandardCompare)
people.sorted(by: sortByFirstName)
/*
[Ava Barnes (2000), Ava Barnes (1998), David Gray (1991),
Emily Young (2002), Joanne Miller (1994), Robert Barnes (1985)]
*/
现在,SortDescriptor 和 NSSortDescriptor 就拥有了同样地表达能力,不过它是类型安全的,而且不依赖于运行时编程。
目前我们只能用一个单一的 SortDescriptor 函数对数组进行排序。如果你还记得,我们曾经用 NSArray.sortedArray(using:) 方法指定了多个比较运算符对数组进行排序。给 Array 甚至是 Sequence 协议添加类似的功能也很简单。不过,我们要添加两个版本的 sort:一个用于原地排序,另一个返回排序后的新数组。
为了避免添加多个扩展,我们使用了一种不同的实现方式:我们定义了一个把多个排序描述符合并为一个的函数。它的工作方式和 sortedArray(using:) 类似:首先它会使用第一个描述符,并检查比较的结果。如果相等,再使用第二个,第三个,直到全部用完:
func combine<Root>
(sortDescriptors: [SortDescriptor<Root>]) -> SortDescriptor<Root> {
return { lhs, rhs in
for areInIncreasingOrder in sortDescriptors {
if areInIncreasingOrder(lhs, rhs) { return true }
if areInIncreasingOrder(rhs, lhs) { return false }
}
return false
}
}
现在我们可以最终把一开始的例子重写为这样:
let combined: SortDescriptor<Person> = combine(
sortDescriptors: [sortByLastName, sortByFirstName, sortByYear]
)
people.sorted(by: combined)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)]
*/
最终,我们得到了一个与 Foundation 中的版本在行为和功能上等价的实现方法,但是我们的方式要更安全,也更符合 Swift 的语言习惯。因为 Swift 的版本不依赖于运行时编程,所以编译器有机会对它进行更好的优化。另外,我们也可以使用它排序结构体或非 Objective-C 的对象。
基于函数的方式有一个不足,那就是函数是不透明的。我们可以获取一个 NSSortDescriptor 并将它打印到控制台,我们也能从排序描述符中获得一些信息,比如键路径,selector 的名字,以及排序顺序等。但是在基于函数的方式中,这些都无法做到。如果这些信息很重要的话,我们可以将函数封装到一个结构体或类中,然后在其中存储一些额外的调试信息。
把函数作为数据使用的这种方式 (例如:在运行时构建包含排序函数的数组),把语言的动态行为带到了一个新的高度。这使得像 Swift 这种需要编译的静态语言也可以实现诸如 Objective-C 或 Ruby 中的一部分动态特性。
我们也看到了合并其他函数的函数的用武之地,它也是函数式编程的构建模块之一。例如,combine(sortDescriptors:) 函数接受一个排序描述符的数组,并将它们合并成了单个的排序描述符。在很多不同的应用场景下,这项技术都非常强大。
除此之外,我们甚至还可以写一个自定义的运算符,来合并两个排序函数:
infix operator <||> : LogicalDisjunctionPrecedence
func <||><A>(lhs: @escaping (A,A) -> Bool, rhs: @escaping (A,A) -> Bool)
-> (A,A) -> Bool
{
return { x, y in
if lhs(x, y) { return true }
if lhs(y, x) { return false }
// 否则,它们就是一样的,所以我们检查第二个条件
if rhs(x, y) { return true }
return false
}
}
大部分时候,自定义运算符不是什么好主意。因为自定义运算符的名字无法描述行为,所以它们通常都比函数更难理解。不过,当使用得当的时候,它们也会非常强大。有了上面的运算符,我们可以重写合并排序的例子:
let combinedAlt = sortByLastName <||> sortByFirstName <||> sortByYear
people.sorted(by: combinedAlt)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)]
*/
这样的代码读起来非常清晰,而且可能比原来调用函数进行合并的做法更简洁一些。不过这有一个前提,那就是你 (和这段代码的读者) 都已经习惯了该操作符的意义。相比自定义操作符的版本,我们还是倾向于选择 combine(sortDescriptors:) 函数。它在调用方看来更加清晰,而且显然增强了代码的可读性。除非你正在写一些面向特定领域的代码,否则自定义的操作符很可能都是在用牛刀杀鸡。
对比我们的版本,Foundation 中的实现仍旧有一个功能上的优势:不用再写额外的代码就可以处理可选值的情况。比如,如果我们想将 Person 的 last 属性换成可选值字符串,对于使用 NSSortDescriptor 的排序代码,我们什么改变都不需要。
但是基于函数的版本就需要一些额外代码。你应该能猜到接下来会发生什么:我们需要再写一个接受函数作为参数,并返回函数的函数。这个函数可以把类似 localizedStandardCompare 这种接受两个字符串并进行比较的普通函数,提升成比较两个字符串可选值的函数。如果两个比较值都是 nil,那么它们相等。如果左侧的值是 nil,而右侧不是的话,返回升序,相反的时候返回降序。最后,如果它们都不是 nil 的话,我们使用 compare 函数来对它们进行比较:
func lift<A>(_ compare: @escaping (A) -> (A) -> ComparisonResult) -> (A?) -> (A?)
-> ComparisonResult
{
return { lhs in { rhs in
switch (lhs, rhs) {
case (nil, nil): return .orderedSame
case (nil, _): return .orderedAscending
case (_, nil): return .orderedDescending
case let (l?, r?): return compare(l)(r)
}
}
}
}
这让我们能够将一个普通的比较函数“提升” (lift) 到可选值的作用域中,这样它就能够和我们的 sortDescriptor 函数一起使用了。如果你还记得之前的 files 数组,你会知道因为需要处理可选值的问题,按照 fileExtension 对它进行排序的代码十分难看。不过现在有了新的 lift 函数,它就又变得很清晰了:
let compare = lift(String.localizedStandardCompare)
let result = files.sorted(by: sortDescriptor(key: { $0.fileExtension }, by: compare))
result // ["one", "file.c", "file.h", "test.h"]
我们可以为返回 Bool 的函数写一个类似的 lift。在可选值一章中我们提到过,标准库现在不再为可选值提供像是 > 这样的运算符了。因为如果你使用的不小心,可能会产生预想之外的结果,因此它们被删除了。Bool 版本的 lift 函数可以让你轻而易举地将现有的运算符提升为可选值也能使用的函数,以满足你的需求。