招聘

假如你看完觉得这篇文章对你有协助,想和咱们一起共事,欢迎参加字节跳动国际化短视频产品研制团队,团队与岗位介绍在文末

正文开端

现代高级语言中,一般都会对调集类型供给高阶函数来简化开发者的代码,大幅进步代码逻辑的可读性。Swift也是如此,像在Swift的数组中的filterforEachmapcompactMapflatMap, reduce等。接下来让咱们一个一个来看他内部源码,剖析完成逻辑。

  1. filter

filter函数的效果类似一个元素过滤器,传入一个过滤规则的闭包,回来满足过滤条件的数组

在咱们解决调试环境的时分现已得知filter函数会调用内部_filter函数,不过假如咱们去搜filter办法会发现许多地方完成了这个办法,而且完成办法还不太一样。咱们仍是一步一步来。

经过断点,咱们首要会进入如下代码:

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办法能够看到如下代码:

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的界说

@usableFromInline
internal protocol _ArrayProtocol
  : RangeReplaceableCollection, ExpressibleByArrayLiteral
where Indices == Range<Int> {
  ...
}

没看到Sequence,却是看到了之前注释里说的有个功率较低的filter版别的RangeReplaceableCollection。不要紧,一路往上找咱们能够看到如下界说:

public protocol RangeReplaceableCollection<Element>: Collection
where SubSequence: RangeReplaceableCollection {
 ...
}
public protocol Collection<Element>: Sequence {
}

所以这些协议的继承联系是:

Sequence -> Collection -> RangeReplaceableCollection -> _ArrayProtocol

所以注释的意思其实是RangeReplaceableCollection重写了filter,可是功率低,所以我要继续重写调用功率高的Sequence里的filter逻辑。

那么,咱们来看看功率低的和功率高的办法有什么差异

 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接连数组来进步功用
  • 笔者还有疑问的点
  • 已然RangeReplaceableCollection里的filter功率低,为啥不删了他,直接用Sequence

    • forums.swift.org/t/why-not-d…
  1. forEach

forEach函数的效果就像他的函数名字界说,便是一个遍历,每个元素会按顺序逐一调用传入的闭包

  @_semantics("sequence.forEach")
  @inlinable
  public func forEach(
    _ body: (Element) throws -> Void
  ) rethrows {
    for element in self {
      try body(element)
    }
  }

这个原理适当简单了,便是for循环,然后每次循环中去调用传递进来的闭包。

  • 规划的比较好的点
  • filter一样的反常回抛机制

  1. map

map是Swift 调集类型的函数,用于将调集的每一个元素经过某个函数进行转化,然后回来一个新的包含已转化元素的调集

以下是 code for debug:

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // 输出: [1, 4, 9, 16, 25]

当咱们断点调试进入map办法时能够看到如下代码(笔者加了注释):

  @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,让咱们回来的元素类型能够和之前的不一样。

要害点在这儿:

    // 获取调集的开端索引,这边是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)

为什么要搞这么杂乱,我直接这样不行吗?

    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类型
  1. compactMap

compactMap相同用于将调集的元素进行转化,但它和map的差异是会主动移除转化成果为 nil 的元素。即,它不仅能够转化数组元素,还会过滤掉转化成果为 nil 的元素,回来不含 nil 的新数组。

以下是code for debug:

let stringArray: [String] = ["1", "2", "three", "4", "five"]
let intArray: [Int] = stringArray.compactMap { Int($0) }
print(intArray)  // 输出: [1, 2, 4]

当咱们断点调试进入compactMap办法的时分能够看到:

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中。最要害的是这儿:

if let newElement = try transform(element) {
    result.append(newElement)
}

增加了对newElement的判断,空的话就不会append了。

  • 规划的比较好的点
  • compactMap的遍历采用了for element in self 的办法,个人感觉比map的遍历要简练易读。
  1. flatMap

在 Swift 4.1 之前,flatMap 有两个版别,一种用于消除嵌套(平铺数组),另一种用于移除 nil。但在 Swift 4.1 之后,处理 nil 的那个版别被 compactMap 替代了。现在 flatMap 首要用于处理嵌套的数组。

关于Swift前史不了解的或许比较懵,其实咱们上面看的compactMap便是从flatMap里分解出来的,分解的点便是传递的闭包函数会不会回来空,会就走compactMap,不会就走flatMap平铺数组才能。

分解点的规划适当奇妙,让咱们一起来看下,这儿先埋个伏笔。

咱们把之前compactMap的测验代码直接替换成调用flatMap:

let stringArray: [String] = ["1", "2", "three", "4", "five"]
let intArray: [Int] = stringArray.flatMap { Int($0) } //compactMap{Int($0)}
print(intArray)  // 输出: [1, 2, 4]

然后断点调试进入flatMap办法:

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(_:)办法。

好接下去咱们注释掉刚刚的测验代码,添加下面的测验代码:

//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]

当咱们再次断点调试进来的时分,会发现咱们进了别的一个办法

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:)来完成平铺的才能。可是为什么会进这儿呢?

其实仔细看完成会发现,办法界说是有差异的:

_ transform: (Element) throws -> ElementOfResult?
_ transform: (Element) throws -> SegmentOfResult

对,差异便是传递的闭包会不会回来nil,假如编译器判断会回来nil,那么编译阶段就会确定调用上面的办法,不然就调用平铺的办法。确实很简单混淆,难怪apple要别离出compactMap

  • 规划的比较好的点
  • 奇妙的运用了闭包的回来值是否会nil,将接口办法的两个完成别离。不过个人以为这样违反了规划上的单一责任的原则,apple后续做别离是正确的。

  1. reduce

reduce 常用于将所有元素组合成一个值。它运用一个初始的累加值和一个闭包作为参数。闭包接受两个参数,一个是之前调用的成果(关于第一次调用则是初始值),另一个是调集中的元素。回来的成果会在下次调用这个闭包时作为输入参数

code for debug

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, { (total, num) in
    return total + num
})
print(sum) // 输出 15

当咱们断点进入reduce办法的时分能够看到如下代码:

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代码也会有一些启发。别的,了解了高阶函数的原理关于咱们对高阶函数的了解会更深刻,合理运用后续能简化咱们日常的代码。

招聘

假如你看完觉得这篇文章对你有协助,想和咱们一起共事,欢迎参加字节跳动国际化短视频产品研制团队

团队介绍

国际化短视频产品研制团队,旨在完成字节跳动国际化短视频事务的研制工作,建立及维护业界领先的产品。

参加咱们,你能接触到包含用户增长、交际 直播、内容发明、内容消费等中心事务场景,支持产品在全球赛道上高速开展;也能接触到包含服务架构、根底技能等方向上的技能挑战,保障事务继续高质量、高功率、且安全地为用户服务;一起还能为不同事务场景供给全面的技能解决方案,优化各项产品目标及用户体会。

在这儿,有大牛带队与大家一起不断探索前沿,打破幻想空间。在这儿,你的每一行代码都将服务亿万用户。在这儿,团队专业且纯粹,合作氛围相等且轻松。

以下岗位可base上海、杭州、北京。

iOS 高级研制工程师

岗位描绘

  1. 负责国际化短视频产品内容发现方向的iOS研制、功用完成和产品迭代; 2. 与产品规划配合,深度参加需求评定,功用界说,体会优化等要害讨论; 3. 规划杰出的技能架构,推进并优化代码的健壮性、可维护性,并编写明晰的技能文档。

岗位要求

  1. 本科及以上计算机相关专业结业,2年以上相关工作经验;
  2. 有较强的学习才能,将新技能运用到事务实践场景中,推进技能迭代;
  3. 有杰出的编程习气,代码结构明晰,命名标准;
  4. 熟练掌握 Objective-C 或 Swift 语言,熟悉App开发的主流结构和开发形式;
  5. 对软件产品有强烈的责任心,具有杰出的沟通才能和优秀的团队协作才能。