招聘
假如你看完觉得这篇文章对你有协助,想和咱们一起共事,欢迎参加字节跳动国际化短视频产品研制团队,团队与岗位介绍在文末
正文开端
现代高级语言中,一般都会对调集类型供给高阶函数来简化开发者的代码,大幅进步代码逻辑的可读性。Swift
也是如此,像在Swift的数组中的filter
,forEach
,map
,compactMap
,flatMap
, reduce
等。接下来让咱们一个一个来看他内部源码,剖析完成逻辑。
-
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
的默许完成,也便是咱们调用的_filter
是Sequence
的默许完成。这个时分咱们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
规划的比较好的点
-
isIncluded
会throws
反常,经过if try
和rethrows
将反常回抛给调用层处理 - 运用
ContiguousArray
接连数组来进步功用
- 笔者还有疑问的点
-
已然
RangeReplaceableCollection
里的filter
功率低,为啥不删了他,直接用Sequence
的- forums.swift.org/t/why-not-d…
-
forEach
forEach
函数的效果就像他的函数名字界说,便是一个遍历,每个元素会按顺序逐一调用传入的闭包
@_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:
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标准数组输出。
别的,咱们能够发现,函数奇妙的界说了Element
和T
,让咱们回来的元素类型能够和之前的不一样。
要害点在这儿:
// 获取调集的开端索引,这边是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)
。Dictionary
和Set
都是无序了,内部运用哈希映射来拜访元素,并不是从0开端。数组切片就更好了解了,适当于一个数组的子集,更不会从0开端。
可是理论上应该用for element in self
是能够的,后面讲到的compactMap
用的也是这个,个人感觉这样更明晰。
- 规划的比较好的点
- 函数奇妙的界说了
Element
和T
,让咱们回来的元素类型能够和之前的不一样 - 界说在了
Collection
协议中,而且运用通用的index遍历办法适配各种Collection
类型
-
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的遍历要简练易读。
-
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后续做别离是正确的。
-
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 高级研制工程师
岗位描绘
- 负责国际化短视频产品内容发现方向的iOS研制、功用完成和产品迭代; 2. 与产品规划配合,深度参加需求评定,功用界说,体会优化等要害讨论; 3. 规划杰出的技能架构,推进并优化代码的健壮性、可维护性,并编写明晰的技能文档。
岗位要求
- 本科及以上计算机相关专业结业,2年以上相关工作经验;
- 有较强的学习才能,将新技能运用到事务实践场景中,推进技能迭代;
- 有杰出的编程习气,代码结构明晰,命名标准;
- 熟练掌握 Objective-C 或 Swift 语言,熟悉App开发的主流结构和开发形式;
- 对软件产品有强烈的责任心,具有杰出的沟通才能和优秀的团队协作才能。