内功帖 - Swift 中 for in 语法的实现原理

swift 发布于 2018年11月19日

这次和大家继续聊聊 Swift 的相关话题。 大家在使用 Swift 开发的时候,如果需要遍历一个集合元素,最先用到的方式之一就是使用 for in 语法。 对 Swift 有一定了解的朋友都清楚,它是一个比较灵活的语言。 for in 语法其实只是某几个协议的实现,即便是我们自己的开发的代码,也同样能拥有 for in 遍历的能力。 下面咱们就来一起看看吧。

如何使用

了解它的原理之前,我们先来看一段简单的代码:

let numbers = [1,2,3,4,5]

for num in numbers {

print(num)

}

上面这段代码再简单不过,首先定义一个包含 5 个整型数的数组, 然后用 for in 语法遍历它,输出每个元素。 那么在我们看来最平常不过的一段代码背后的原理是否深究过呢? 其实是有章可循的。

实现原理

还是从上面那个例子入手,我们定义的整型数组在 Swift 中对应那个类型呢。 如果我们执行这行代码:

type(of: numbers)

就会得到这个输出,它在 Swift 中精确的类型表示是 Array<Int>。 苹果的官方文档中有对他继承关系的完整说明:

Array 遵循的所有 Protocol 都在这里,基于我们这篇文章的讨论焦点,我们只需要关注最后三个 MutableCollection, RandomAccessCollection 和 RangeReplaceableCollection。

从名字可以看出,他们都是集合类型所对应的协议,至于这三个协议具体的功能,咱们不多做讨论。 我们这里关注的 for in 语法实现所在的层级比他们还要更高一层。

在进入 MutableCollection, RandomAccessCollection 和 RangeReplaceableCollection 这三个 Protocol 的文档中, 都会列出这段内容:

也就是说,他们都继承自 Collection 协议,它是最通用的集合定义。 Collection 还不是最顶层, 它的上面还有一层 Sequence 协议。 那么 Swift 中集合的结构,我们可以简化的表示为这样:

注意上面图片中的继承和实现标注区别, Array 是最终我们使用的 struct 类型, 它实现了几个 Collection 的子协议。 关于整个类结构,不是我们这次要讨论的重点,大家了解即可,这里不再多介绍。

我们这次主题的关键,就在这个结构中的 Sequence 协议中。 在这个协议中,定义了一个 makeIterator() 方法, 顾名思义,它会产生这个集合的迭代器。 迭代器的概念如果有其他语言看法经验的同学,应该并不陌生。 还拿我们开始的数组遍历来举例子:

let numbers = [1,2,3,4,5]

var iter = numbers.makeIterator()

while let num = iter.next() {

print(num)

}

这段代码的输出结果和我们之前用 for in 循环语法是一样的。 其实说白了 for in 语法在编译后,实际上调用的就是上面这段代码。 有了这个认知后,咱们再来看看 makeIterator() 方法的文档定义:

func makeIterator() -> Self.Iterator

它返回的是一个 Self.Iterator 类型的实例。 它是 Sequence 协议的 associatedtype:

associatedtype Iterator : IteratorProtocol

也就是说,这个迭代器实际上是 IteratorProtocol 的实例。 这里就涉及到了好几个概念,如果事先不了解的同学听我这么一说可能会有点懵。 先说 associatedtype,它是定义在 Sequence 协议中的一个相关类型,大家暂且可以把它理解为一个别名机制。 我们在 Sequence 内部用作 Iterator 但它的实际类型是 IteratorProtocol

引入 associatedtype 这个概念的原因是,在实际 Runtime 的时候,makeIterator() 不一定只会返回 IteratorProtocol 的精确实例,也有可能返回它的子协议的实例。 子协议是可以让我们自己定制的。 有点像 Java 中泛型的概念,但不全是,总之如果我这段讲解还没有让你理解它,就可以暂时跳过,后面的内容也会帮助你理解它。

Sequence 协议这部分的代码,结构化起来应该是这样:

public protocol Sequence {

associatedtype Iterator : IteratorProtocol

func makeIterator() -> Iterator

}
实现自定义的 for in 语法

上面我们对 Sequence 协议做了一个大体的了解。 现在我们试着实现一个自定义的集合类型,并且能够支持 for in 语法:

struct Countdown : Sequence {

let start: Int

func makeIterator() -> CountdownIterator {

return CountdownIterator(self)

}

}

Countdown 使我们自定义的类型, 它实现了 Sequence 协议, 定义了一个 start 属性,并且也实现了 makeIterator 方法。 这里面返回了一个 CountdownIterator。 很显然这是我们自定义的一个迭代器。

我们再来看看 CountdownIterator 的定义:

struct CountdownIterator : IteratorProtocol {

let countdown: Countdown
var times = 0

init(_ countdown: Countdown) {

self.countdown = countdown

}

mutating func next() -> Int? {

let nextNumber = countdown.start - times
guard nextNumber > 0
else { return nil }

times += 1
return nextNumber

}


}

首先,在构造方法里面, 反向引用了 Countdown 实例。 然后实现了 next 方法, 每次调用都会把 countdown 的差值减一,直到 0 为止。 我用一句话概括一下它们的工作机制。

首先实现了 Sequence 的子类 Countdown 只负责集合概念内的事情, 如果需要遍历这个集合的时候,就会调用它的 makeIterator() 方法生成迭代器。 迭代器自身只负责迭代本身这个领域的操作,它提供一个 next() 方法供控制层调用,只要集合中还有内容, next() 方法就会返回一个实际的元素。 如果没有再能遍历的内容了, 它就会返回 nil。 在咱们上面的代码中,大家也能看到了。 当然,迭代器具体的逻辑如何实现,是由我们来控制的,只需要符合 next() 方法的协议特性即可。

定义完成了,这时候可以试试直接用 for in 循环来遍历我们自己的集合类:

var ct = Countdown(start: 5)

for item in ct {

print(item)

}

在命令行中, 会输出从 5 到 1 的数字。 正是我们这个集合的遍历逻辑。

总结

Swift 本身是一个非常灵活的语言。 有人说 Swift 容易学习,我只能说它的入门可能看起来很容易,但我个人多年的开发经验对它的感觉,其实它还是蛮复杂的一个变成语言。至少你要精通它还是要花上不少功夫的。 但是,一旦你透彻了解它的原理后,又会发现它的设计非常有艺术感。 比如我们这次和大家聊得 for in 循环,以及从这个语法中引出的集合类型以及他们的层级关系,其实有不少细节上可以深入讨论的地方,只是限于篇幅原因,实在没办法全部展开。也许这篇内容能够激活大家的讨论,也欢迎留言交流。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe