【发布时间】:2016-10-13 00:15:02
【问题描述】:
注意:我目前仍在使用 Swift 2.2,但也对 Swift 3 解决方案开放
我希望创建一个与filter 非常接近的函数,除了它还保留不匹配的结果并保持排序顺序。例如,假设您想过滤掉数组中可被 3 整除的数字,但仍保留不能被 3 整除的数字列表。
选项 1:使用filter
使用filter,您只能获得可被 3 整除的数字列表,而原始列表保持不变。然后,您可以使用相反的谓词再次过滤原始列表,但这是不必要的第二遍。代码如下所示:
let numbers = [1,2,3,4,5,6,7,8,9,10]
let divisibleBy3 = numbers.filter { $0 % 3 == 0 } // [3,6,9]
let theRest = numbers.filter { $0 % 3 != 0 } // [1,2,4,5,7,8,10]
这确实是非常易读的,但它执行 2 遍的事实对我来说似乎效率低下,尤其是在谓词更复杂的情况下。这是实际需要的两倍。
选项 2:在 Collection 扩展中使用自定义 separate 函数
我的下一个尝试是扩展Collection 并创建一个我称为separate 的函数。此函数将获取集合并一次遍历一个元素并将它们添加到匹配列表或不匹配列表中。代码如下所示:
extension Collection {
func separate(predicate: (Generator.Element) -> Bool) -> (matching: [Generator.Element], notMatching: [Generator.Element]) {
var groups: ([Generator.Element],[Generator.Element]) = ([],[])
for element in self {
if predicate(element) {
groups.0.append(element)
} else {
groups.1.append(element)
}
}
return groups
}
}
然后我可以像这样使用函数:
let numbers = [1,2,3,4,5,6,7,8,9,10]
let groups = numbers.separate { $0 % 3 == 0 }
let matching = groups.matching // [3,6,9]
let notMatching = groups.notMatching // [1,2,4,5,7,8,10]
这也很干净,但我唯一不喜欢的是我使用元组作为返回类型。也许其他人会不同意,但我更喜欢返回与self 相同的类型进行链接。但从技术上讲,你可以抓住.matching 或.notMatching,这与self 的类型相同,你可以链接它们中的任何一个。
选项 3:在 Array 扩展中使用自定义的变异 removeIf 函数
separate 返回元组的问题导致我尝试创建一个函数来修改 self,方法是删除找到的匹配项并将它们添加到新列表中,并在最后返回匹配项列表.返回的列表是您的匹配项,并且该数组被修剪掉这些值。两个数组中都保留了顺序。代码如下所示:
extension Array {
mutating func removeIf(predicate: (Element) -> Bool) -> [Element] {
var removedCount: Int = 0
var removed: [Element] = []
for (index,element) in self.enumerated() {
if predicate(element) {
removed.append(self.remove(at: index-removedCount))
removedCount += 1
}
}
return removed
}
}
它是这样使用的:
var numbers = [1,2,3,4,5,6,7,8,9,10]
let divisibleBy3 = numbers.removeIf { $0 % 3 == 0 }
// divisibleBy3: [3,6,9]
// numbers: [1,2,4,5,7,8,10]
这个函数必须在Array的扩展中实现,因为删除特定索引处的元素的概念不适用于常规Collections(Array定义为public struct Array<Element> : RandomAccessCollection, MutableCollection,并且它直接定义了remove(at:)函数,而不是从继承或协议中获取)。
选项 4:选项 2 和 3 的组合
我是代码重用的忠实拥护者,在提出选项 3 后,我意识到我可能可以重用选项 2 中的 separate 函数。我想出了这个:
extension Array {
mutating func removeIf(predicate: (Element) -> Bool) -> [Element] {
let groups = self.separate(predicate: predicate)
self = groups.notMatching
return groups.matching
}
}
并且它的使用方式与选项 3 中一样。
我关心性能,所以我通过 XCTest 的measure 运行每个选项,迭代 1000 次。结果如下:
Option 1: 9 ms
Option 2: 7 ms
Option 3: 10 ms
Option 4: 8 ms
选项 5:基于 negaipro 的回答
我知道partition,但我不打算考虑它,因为它没有保留排序顺序。 negaipro 的回答基本上是partition,但它让我思考。 partition 的想法是交换与枢轴点匹配的元素,从而确保结束枢轴点一侧的所有内容都与谓词匹配,而另一侧则不匹配。我接受了这个想法并将动作更改为“移动到最后”。因此,匹配项会从他们的位置移除并添加到末尾。
extension Array {
mutating func swapIfModified(predicate: (Element) -> Bool) -> Int {
var matchCount = 0
var index = 0
while index < (count-matchCount) {
if predicate(self[index]) {
append(remove(at: index))
matchCount += 1
} else {
index += 1
}
}
return count-matchCount
}
}
在我使用包含 10 个数字的数组的初始测试中,它与其他选项相当。但我担心append(remove(at: index)) 行的性能。所以我再次尝试了所有选项,数组从 1 到 1000,这个选项绝对是最慢的。
结论:
这些选项之间没有很大的性能差异。而且由于选项 4 比选项 3 更快并且重用选项 2 中的代码,所以我倾向于抛弃选项 3。所以当我不关心未过滤的结果时,我倾向于使用普通的旧 filter (同样,当我不关心过滤结果时,因为它只是使用相反的谓词),然后在我关心保留过滤和未过滤结果时使用separate 或removeIf。
问题:
那么,我是否遗漏了 Swift 中已经内置的东西?有没有更好的方法来实现这一点?我的扩展语法是否缺少任何东西(例如,任何可以使它将此概念应用于更多领域的东西)?
【问题讨论】:
-
减少!!!!!! :D
-
请进一步解释。如何减少我正在寻找的东西?我需要 2 个数组。 Reduce 生成单个元素。
-
如果你的单个元素是两个数组的数组会怎样
-
在我看来,这并不比 2 元组好多少。实际上,我认为它更糟糕,因为 2 元组保证只有 2 个值,但数组数组中可以包含任意数量的数组。
-
在这种情况下,一个元组会更好,那么如果你的单个元素是一个
([Int], [Int])的元组呢?