Swift语言底层原理剖析-Array系列-高阶函数

现代高级语言中,一般都会对集合类型提供高阶函数来简化开发者的代码,大幅提高代码逻辑的可读性。Swift也是如此,像在Swift的数组中的filterforEachmapcompactMapflatMap, 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 的默认实现,也就是我们调用的_filterSequence的默认实现。这个时候我们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设计的比较好的点
  • isIncludedthrows异常,通过if tryrethrows将异常回抛给调用层处理
  • 使用ContiguousArray连续数组来提高性能
  • 笔者还有疑惑的点

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标准数组输出。

另外,我们可以发现,函数巧妙的定义了ElementT,让我们返回的元素类型可以和之前的不一样。

关键点在这里:

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)DictionarySet都是无序了,内部使用哈希映射来访问元素,并不是从0开始。数组切片就更好理解了,相当于一个数组的子集,更不会从0开始。

但是理论上应该用for element in self 是可以的,后面讲到的compactMap用的也是这个,个人感觉这样更清晰。

  • 设计的比较好的点
  • 函数巧妙的定义了ElementT,让我们返回的元素类型可以和之前的不一样
  • 定义在了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代码也会有一些启发。另外,了解了高阶函数的原理对于我们对高阶函数的理解会更深刻,合理使用后续能简化我们日常的代码。

相关推荐
良技漫谈14 小时前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
KeithTsui1 天前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift
袁代码2 天前
Swift 开发教程系列 - 第4章:函数与闭包
ios·swift·ios开发
安泽13143 天前
高德地图美食
开发语言·swift·美食
袁代码3 天前
Swift 开发教程系列 - 第2章:Swift 基础语法
swift·ios开发·基础教程
袁代码3 天前
Swift 开发教程系列 - 第1章:Swift 简介与开发环境配置
swift·ios开发·基础教程
孚亭3 天前
一些swift问题
swift
莫问alicia3 天前
echarts 实现3D饼状图 加 label标签显示
前端·3d·echarts·swift
uiop_uiop_uiop6 天前
iOS Swift5算法恢复——HMAC
ios·iphone·swift
東三城7 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节