filter和与filter类似的语义
之前,我们提到过filter
的用法,用于在Array
中,过滤满足特定条件的元素。而这个条件,就是通过filter
的closure参数来确定的:
var fibonacci = [0, 1, 1, 2, 3, 5]
// [0, 2]
fibonacci.filter { $0 % 2 == 0 }
按照上一节中实现map
的思路,我们可以自己来实现一个filter
:
extension Array {
func myFilter(_ predicate: (Element) -> Bool) -> [Element] {
var tmp: [Element] = []
for value in self where predicate(value) {
tmp.append(value)
}
return tmp
}
}
在上面的实现里,最核心的环节就是通过带有where
条件的for
循环找到原数组中符合条件的元素,然后把它们一一添加到tmp
中,并最终返回给函数的调用者。然后,我们测试下myFilter
:
fibonacci.myFilter { $0 % 2 == 0 } // [0, 2]
结果,应该是和标准库中自带的filter
是一样的。理解了filter
之后,我们就可以自行定义一些标准库中没有的方法。例如:
剔除掉数组中满足条件的元素:
extension Array {
func reject(_ predicate: (Element) -> Bool) -> [Element] {
return filter { !predicate($0) }
}
}
我们只要把调用转发给filter
,然后把指定的条件取反就好了。这样,剔除元素的代码语义上就会更好看一些:
fibonacci.reject { $0 % 2 == 0 } // [1, 1, 3, 5]
另一个基于filter
语义的常用操作是判断数组中是否存在满足条件的元素。下面的代码可以完成任务:
fibonacci.filter { $0 % 2 == 0 }.count > 0 // true
但这样做在性能上并不理想,因为即便找到了满足条件的元素,也要遍历完整个数组,这显然是没必要的。Swift标准库中,提供了一个更方便的方法:
fibonacci.contains { $0 % 2 == 0 } // true
contains
的一个好处就是只要遇到满足条件的元素,函数的执行就终止了。基于这个contains
,我们还可以给Array
添加一个新的方法,用来判断Array
中所有的元素是否满足特定的条件:
extension Array {
func allMatch(_ predicate: (Element) -> Bool) -> Bool {
return !contains { !predicate($0) }
}
}
在allMatch
的实现里,只要没有不满足条件的元素,也就是所有元素都满足条件了。我们可以用下面的代码测试一下:
let evens = [2, 4, 6, 8]
evens.allMatch { $0 % 2 == 0 } // true
reduce和与reduce相关的语义
除了用一个数组生成一个新的数组,有时,我们会希望把一个数组变成某种形式的值。例如,之前我们提到的求和:
fibonacci.reduce(0, +) // 12
了解reduce
的进一步用法之前,我们先来自己实现一个:
extension Array {
func myReduce(_ initial: T, _ next: (T, Element) -> T) -> T {
var tmp = initial
for value in self {
tmp = next(tmp, value)
}
return tmp
}
}
从上面的实现就可以看出,reduce
的实现也没有什么神奇之处。无非就是把for
循环迭代相加的过程封装了起来。然后,用下面的代码测试一下,就会发现和标准库中的reduce
一样了。
fibonacci.myReduce(0, +) // 12
除了求和之外,我们还可以把fibonacci
reduce成一个字符串:
let str = fibonacci.myReduce("") { str, num in
return str + "\(num) "
}
// "0 1 1 2 3 5 "
甚至,我们还可以用reduce
模拟map
和filter
的实现:
extension Array {
func myMap2(_ transform: (Element) -> T) -> [T] {
return reduce([], { $0 + [transform($1)] })
}
func myFilter2(_ predicate: (Element) -> Bool) -> [Element] {
return reduce([], { predicate($1) ? $0 + [$1] : $0 })
}
}
然后,简单测试一下:
// [0, 1, 1, 4, 9, 25]
fibonacci.myMap2 { $0 * $0 }
// [0, 2]
fibonacci.myFilter2 { $0 % 2 == 0 }
它们的结果和标准库中的map
和filter
是一样的。但是,这种看似优雅的写法却没有想象中的那么好。在它们内部的reduce
调用中,每一次$0
的参数都是一个新建的数组,因此整个算法的复杂度是O(n2),而不再是for
循环版本的O(n)。所以,这样的实现方法最好还是用来作为理解reduce
用法的例子。
flatMap
最后,我们来了解flatMap
。简单来说,如果你用在map
中的closure参数不返回一个数组元素,而是也返回一个数组,这样,你就会得到一个数组的数组,但如果你只需要一个一维数组,flatMap
就可以派上用场了,而这,也就是flat的含义。先来看一个例子:
假设,我们要给animals
数组中的动物都使用ids
中的数字进行编号。一开始,可能我们会写下这样的代码:
animals.map { animal in
return ids.map { id in (animal, id) }
}
但如果是这样,由于animals.map
使用的closure参数返回的是一个Array
,而不是单一元素,最终,我们会得到一个“数组的数组”:
但这并不是我们想要的,我们只是需要一个一维数组表示所有的(animal, id)
。此时,就可以让flatMap
派上用场了:
animals.flatMap { animal in
return ids.map { id in (animal, id) }
}
这样,我们就能得到期望的内容了:
实际上,flatMap
的实现很简单,只要在map
内部的for
循环里,不断把closure参数生成的数组的内容,添加到要返回的结果里就好了:
extension Array {
func myFlatMap(_ transform: (Element) -> [T]) -> [T] {
var tmp: [T] = []
for value in self {
tmp.append(contentsOf: transform(value))
}
return tmp
}
}
用下面的代码测试,得到的结果,应该和之前使用flatMap
是一样的:
animals.myFlatMap { animal in
return ids.map { id in (animal, id) }
}