现代高级语言中,一般都会对集合类型提供高阶函数来简化开发者的代码,大幅提高代码逻辑的可读性。Swift
也是如此,像在Swift的数组中的filter
,forEach
,map
,compactMap
,flatMap
, reduce
等。接下来让我们一个一个来看他内部源码,剖析实现逻辑。
filter
filter
函数的作用类似一个元素过滤器,传入一个过滤规则的闭包,返回满足过滤条件的数组
在我们解决调试环境的时候已经得知filter
函数会调用内部_filter
函数,不过如果我们去搜filter
方法会发现好多地方实现了这个方法,而且实现方式还不太一样。我们还是一步一步来。
通过断点,我们首先会进入如下代码:
swift
extension _ArrayProtocol {
// Since RangeReplaceableCollection now has a version of filter that is less
// efficient, we should make the default implementation coming from Sequence
// preferred.
@inlinable
public __consuming func filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
return try _filter(isIncluded)
}
}
先不着急着进_filter
,看看注释,大致意思是说由于 RangeReplaceableCollection
现在有一个效率较低的过滤器版本,我们应该首选来自 Sequence
的默认实现。盲猜感觉是_ArrayProtocol
的父协议,父协议的_filter
实现的不好,然后要用Sequence
的默认实现,也就是我们调用的_filter
是Sequence
的默认实现。这个时候我们step into_filter
方法可以看到如下代码:
swift
extension Sequence {
@inlinable
public __consuming func filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
return try _filter(isIncluded)
}
public func _filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
// 定义一个连续数组`result`,用于存储过滤后的元素
var result = ContiguousArray<Element>()
// 使用`makeIterator()`方法创建一个迭代器,用于访问集合中的每一个元素
var iterator = self.makeIterator()
// 使用`while let`结构,通过调用迭代器的`next()`方法在集合中不断取下一个元素
while let element = iterator.next() {
// 如果闭包`isIncluded`返回`true`,那么就将这个元素添加到`result`数组中
if try isIncluded(element) {
result.append(element)
}
}
// 将连续数组`result`转换为数组并返回
return Array(result)
}
}
_filter
本身的逻辑不复杂,笔者也加了注释,创建一个ContiguousArray
数组,然后通过迭代器逐个迭代匹配,能够匹配到就放到ContiguousArray
中,最后将ContiguousArray
转成Array
返回。
这边主要还是剖析其他内容,首先至少说明_ArrayProtocol
协议继承自Sequence
,不然咱们进不到这个方法.另外我们看到Sequence
里有_ArrayProtocol
一模一样的filter
方法,先不纠结,我们来看看_ArrayProtocol
的定义
kotlin
@usableFromInline
internal protocol _ArrayProtocol
: RangeReplaceableCollection, ExpressibleByArrayLiteral
where Indices == Range<Int> {
...
}
没看到Sequence
,倒是看到了之前注释里说的有个效率较低的filter
版本的RangeReplaceableCollection
。没关系,一路往上找我们可以看到如下定义:
swift
public protocol RangeReplaceableCollection<Element>: Collection
where SubSequence: RangeReplaceableCollection {
...
}
public protocol Collection<Element>: Sequence {
}
所以这些协议的继承关系是:
Sequence
-> Collection
-> RangeReplaceableCollection
-> _ArrayProtocol
。
所以注释的意思其实是RangeReplaceableCollection
重写了filter
,但是效率低,所以我要继续重写调用效率高的Sequence
里的filter
逻辑。
那么,我们来看看效率低的和效率高的方法有什么区别
swift
extension RangeReplaceableCollection {
//效率低的
@inlinable
@available(swift, introduced: 4.0)
public __consuming func filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> Self {
var result = Self()
for element in self where try isIncluded(element) {
result.append(element)
}
return result
}
}
extension Sequence {
//效率高的
public func _filter(
_ isIncluded: (Element) throws -> Bool
) rethrows -> [Element] {
// 定义一个连续数组`result`,用于存储过滤后的元素
var result = ContiguousArray<Element>()
// 使用`makeIterator()`方法创建一个迭代器,用于访问集合中的每一个元素
var iterator = self.makeIterator()
// 使用`while let`结构,通过调用迭代器的`next()`方法在集合中不断取下一个元素
while let element = iterator.next() {
// 如果闭包`isIncluded`返回`true`,那么就将这个元素添加到`result`数组中
if try isIncluded(element) {
result.append(element)
}
}
// 将连续数组`result`转换为数组并返回
return Array(result)
}
}
这边主要的区别就是一个是用ContiguousArray
连续数组来提高性能,ContiguousArray
可以更好的利用CPU缓存,尤其在处理大量数据时会更加明显,内部细节咱们可以后续探究,暂时先不展开。另外一个是用迭代器来替代for-in
循环,目前了解到的信息(From GPT4.0)for-in
底层实际上也是使用 iterator 来实现的,这边估计对性能应该没啥影响,这个实现应该在编译阶段就处理了。
- 笔者认为
filter
设计的比较好的点
isIncluded
会throws
异常,通过if try
和rethrows
将异常回抛给调用层处理- 使用
ContiguousArray
连续数组来提高性能
- 笔者还有疑惑的点
-
既然
RangeReplaceableCollection
里的filter
效率低,为啥不删了他,直接用Sequence
的
forEach
forEach
函数的作用就像他的函数名字定义,就是一个遍历,每个元素会按顺序逐个调用传入的闭包
less
@_semantics("sequence.forEach")
@inlinable
public func forEach(
_ body: (Element) throws -> Void
) rethrows {
for element in self {
try body(element)
}
}
这个原理相当简单了,就是for
循环,然后每次循环中去调用传递进来的闭包。
- 设计的比较好的点
- 和
filter
一样的异常回抛机制
map
map
是Swift 集合类型的函数,用于将集合的每一个元素通过某个函数进行转换,然后返回一个新的包含已转换元素的集合
以下是 code for debug:
ini
let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // 输出: [1, 4, 9, 16, 25]
当我们断点调试进入map方法时可以看到如下代码(笔者加了注释):
swift
@inlinable
public func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] {
// 获取集合元素的数量
let n = self.count
// 如果元素数量为0,则直接返回一个空数组
if n == 0 {
return []
}
// 初始化一个连续数组结果,用于储存转换后的元素
var result = ContiguousArray<T>()
// 因为总数已知,预先分配内存空间,以优化性能
result.reserveCapacity(n)
// 获取集合的起始索引,这边是array,其实就是0。如果是一些数组切片的类这里就不是0了
var i = self.startIndex
// 遍历集合中的每个元素
for _ in 0..<n {
// 应用转换函数并将结果添加到结果数组
result.append(try transform(self[i]))
// 更新索引
formIndex(after: &i)
}
// 检查索引是否已经到达集合的末端
_expectEnd(of: self, is: i)
// 返回结果数组,将其从ContiguousArray转化为标准数组
return Array(result)
}
代码逻辑不难理解,首先是边界检查,然后开辟一个ContiguousArray
用来储存转换后的元素,因为总数已知,所以通过reserveCapacity
提前分配好内存空间。然后就是通过索引去遍历整个数组,遍历中去调用transform
闭包做元素mapping后append到ContiguousArray
实例中,最后转换成Array标准数组输出。
另外,我们可以发现,函数巧妙的定义了Element
和T
,让我们返回的元素类型可以和之前的不一样。
关键点在这里:
php
// 获取集合的起始索引,这边是array,其实就是0。如果是一些数组切片的类这里就不是0了
var i = self.startIndex
// 遍历集合中的每个元素
for _ in 0..<n {
// 应用转换函数并将结果添加到结果数组
result.append(try transform(self[i]))
// 更新索引
formIndex(after: &i)
}
// 检查索引是否已经到达集合的末端
_expectEnd(of: self, is: i)
为什么要搞这么复杂,我直接这样不行吗?
css
for i in 0..<n {
result.append(try transform(self[i]))
}
后来发现,map
方法是定义在Collection
协议中的默认实现,还真不行,因为实现这个协议的除了Array
,还有字典(Dictionary)
,集合(Set)
,有子集范围的数组(Array Slices)
。Dictionary
和Set
都是无序了,内部使用哈希映射来访问元素,并不是从0开始。数组切片就更好理解了,相当于一个数组的子集,更不会从0开始。
但是理论上应该用for element in self
是可以的,后面讲到的compactMap
用的也是这个,个人感觉这样更清晰。
- 设计的比较好的点
- 函数巧妙的定义了
Element
和T
,让我们返回的元素类型可以和之前的不一样 - 定义在了
Collection
协议中,并且使用通用的index遍历方式适配各种Collection
类型
compactMap
compactMap
同样用于将集合的元素进行转换,但它和map的区别是会自动移除转换结果为 nil 的元素。即,它不仅可以转换数组元素,还会过滤掉转换结果为 nil 的元素,返回不含 nil 的新数组。
以下是code for debug:
javascript
let stringArray: [String] = ["1", "2", "three", "4", "five"]
let intArray: [Int] = stringArray.compactMap { Int($0) }
print(intArray) // 输出: [1, 2, 4]
当我们断点调试进入compactMap
方法的时候可以看到:
swift
extension Sequence {
@inlinable
public func compactMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}
@inlinable
@inline(__always)
public func _compactMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
// 创建一个空数组 result,此数组用于存储经过转换并成功解包的元素。
var result: [ElementOfResult] = []
// 使用 for-in 循环遍历 Sequence 中的每一个元素。
for element in self {
// 尝试用闭包 transform 来转换每一个元素,
// 如果转换成功并且结果不为 nil,则将解包后的结果添加到结果数组中。
if let newElement = try transform(element) {
result.append(newElement)
}
}
// 返回结果数组。
return result
}
}
可以看到compactMap
调用了内部的_compactMap
方法,正在的实现在_compactMap
中。最关键的是这里:
javascript
if let newElement = try transform(element) {
result.append(newElement)
}
增加了对newElement
的判断,空的话就不会append
了。
- 设计的比较好的点
compactMap
的遍历采用了for element in self
的方式,个人感觉比map的遍历要简洁易读。
flatMap
在 Swift 4.1 之前,flatMap
有两个版本,一种用于消除嵌套(平铺数组),另一种用于移除 nil。但在 Swift 4.1 之后,处理 nil 的那个版本被 compactMap 取代了。现在 flatMap 主要用于处理嵌套的数组。
对于Swift历史不了解的可能比较懵,其实我们上面看的compactMap
就是从flatMap里分化出来的,分化的点就是传递的闭包函数会不会返回空,会就走compactMap
,不会就走flatMap
的平铺数组
能力。
分化点的设计相当巧妙,让我们一起来看下,这里先埋个伏笔。
我们把之前compactMap
的测试代码直接替换成调用flatMap
:
bash
let stringArray: [String] = ["1", "2", "three", "4", "five"]
let intArray: [Int] = stringArray.flatMap { Int($0) } //compactMap{Int($0)}
print(intArray) // 输出: [1, 2, 4]
然后断点调试进入flatMap
方法:
swift
extension Sequence {
@available(swift, deprecated: 4.1/*, obsoleted: 5.1 */, renamed: "compactMap(_:)",
message: "Please use compactMap(_:) for the case where closure returns an optional value")
public func flatMap<ElementOfResult>(
_ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}
}
很简单直接左手转右手调用了compactMap
一样调用的_compactMap
方法。从定义上看apple更希望这种情况让我们直接调用compactMap(_:)
方法。
好接下去我们注释掉刚刚的测试代码,添加下面的测试代码:
bash
//let stringArray: [String] = ["1", "2", "three", "4", "five"]
//let intArray: [Int] = stringArray.flatMap { Int($0) }
//print(intArray) // 输出: [1, 2, 4]
let nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let flatArray = nestedArray.flatMap { $0 }
print(flatArray) // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
当我们再次断点调试进来的时候,会发现我们进了另外一个方法
swift
extension Sequence {
@inlinable
public func flatMap<SegmentOfResult: Sequence>(
_ transform: (Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
}
方法体实现不难理解,就是遍历后通过调用append(contentsOf:)
来实现平铺的能力。但是为什么会进这里呢?
其实仔细看实现会发现,方法定义是有区别的:
java
_ transform: (Element) throws -> ElementOfResult?
_ transform: (Element) throws -> SegmentOfResult
对,区别就是传递的闭包会不会返回nil,如果编译器判断会返回nil,那么编译阶段就会确定调用上面的方法,否则就调用平铺的方法。的确很容易混淆,难怪apple要分离出compactMap
- 设计的比较好的点
- 巧妙的运用了闭包的返回值是否会nil,将接口方法的两个实现分离。不过个人认为这样违背了设计上的单一职责的原则,apple后续做分离是正确的。
reduce
reduce
常用于将所有元素组合成一个值。它使用一个初始的累加值和一个闭包作为参数。闭包接受两个参数,一个是之前调用的结果(对于第一次调用则是初始值),另一个是集合中的元素。返回的结果会在下次调用这个闭包时作为输入参数
code for debug
bash
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, { (total, num) in
return total + num
})
print(sum) // 输出 15
当我们断点进入reduce方法的时候可以看到如下代码:
swift
extension Sequence {
@inlinable
public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult:
(_ partialResult: Result, Element) throws -> Result
) rethrows -> Result {
// 初始化累加器为 initialResult
var accumulator = initialResult
// 使用 for-in 循环来遍历序列中的每一项
for element in self {
// 使用闭包 nextPartialResult 来处理每一个元素,更新accumulator的值
accumulator = try nextPartialResult(accumulator, element)
}
// 返回累加器的结果,也就是把所有元素reduce之后的结果
return accumulator
}
}
逻辑并不复杂,通过持续的传入accumulator
给累加器来达到累加的目的。
总结一下
Array系列的高阶函数其实是Collection的高阶函数,同样适合与Dictionary,Set等其他集合类型。整体的设计也比较巧妙,用到了很多Swift特有的Protocol特性,对我们日后设计Swift代码也会有一些启发。另外,了解了高阶函数的原理对于我们对高阶函数的理解会更深刻,合理使用后续能简化我们日常的代码。