大家好 今天,我们要分享在“ iOS开发者”课程启动前夕准备的翻译。 高级课程 。 ” 走吧

Swift基于协议的设计的主要优点之一是,它使我们能够编写与各种类型兼容的通用代码,而不是专门为每个人实现。 特别是如果此类通用代码用于一种协议,可以在标准库中找到,这将允许将其与内置类型和用户定义类型一起使用。
这种协议的一个示例是Sequence,它可以被所有可以迭代的标准库类型接受,例如Array,Dictionary,Set等。 本周,让我们看看如何将Sequence包装在通用容器中,这将使我们能够将各种算法封装在易于使用的API的核心。
懒惰的艺术
认为所有序列都与Array相似,很容易造成混淆,因为创建序列时所有元素都会立即加载到内存中。 由于序列协议的唯一要求是接收者必须能够迭代,因此我们无法对未知序列的元素如何加载或存储进行任何假设。
例如,正如我们在“ 快速序列:懒惰的艺术”中所介绍的那样,出于性能原因或由于不能保证整个序列都可以容纳在内存中,序列有时可能会延迟加载其元素。 以下是此类序列的一些示例:
由于上述所有序列由于某种原因都是惰性的,因此我们不希望例如通过调用Array(folder.subfolders)将其强制为数组。 但是我们仍然可能希望以不同的方式进行修改和使用它们,因此让我们看一下如何通过创建一种序列包装器来做到这一点。
基金会创建
首先创建一个基本类型,该基本类型可用于在任何序列之上创建各种便捷的API。 我们将其称为WrappedSequence,它将是一个通用类型,既包含我们包装的序列的类型,又包含我们希望新序列创建的元素的类型。
包装器的主要功能是IteratorFunction,它使我们能够控制基本序列的搜索-更改每次迭代所使用的Iterator:
struct WrappedSequence<Wrapped: Sequence, Element>: Sequence { typealias IteratorFunction = (inout Wrapped.Iterator) -> Element? private let wrapped: Wrapped private let iterator: IteratorFunction init(wrapping wrapped: Wrapped, iterator: @escaping IteratorFunction) { self.wrapped = wrapped self.iterator = iterator } func makeIterator() -> AnyIterator<Element> { var wrappedIterator = wrapped.makeIterator() return AnyIterator { self.iterator(&wrappedIterator) } } }
如上所示,Sequence使用一种工厂模式,以便每个序列使用makeIterator()方法为每次迭代创建一个新的迭代器实例。
上面,我们使用标准库的AnyIterator类型,它是类型擦除的迭代器 ,可以使用任何基本的IteratorProtocol实现来获取Element值。 在我们的例子中,我们将通过调用IteratorFunction来创建一个元素,将包装的序列的我们自己的迭代器作为参数传递,并且由于将此参数标记为inout,我们可以在函数内部就地更改基础迭代器。
由于WrappedSequence也是一个序列,因此我们可以使用与其相关的标准库的所有功能,例如对其进行迭代或使用map转换其值:
let folderNames = WrappedSequence(wrapping: folders) { iterator in return iterator.next()?.name } for name in folderNames { ... } let uppercasedNames = folderNames.map { $0.uppercased() }
现在让我们开始使用我们的新WrappedSequence!
前缀和后缀
当经常使用序列时,希望在我们使用的序列中插入前缀或后缀-但是如果我们不改变主序列就可以这样做会不是很好吗? 这可以导致更好的性能,并允许我们向任何序列添加前缀和后缀,而不仅是诸如Array之类的常规类型。
使用WrappedSequence,我们可以很容易地做到这一点。 我们需要做的就是使用一种方法扩展Sequence,该方法从元素数组创建包装的序列以作为前缀插入。 然后,当我们进行迭代时,我们开始对所有前缀元素进行迭代,然后再继续执行基本序列,如下所示:
extension Sequence { func prefixed( with prefixElements: Element... ) -> WrappedSequence<Self, Element> { var prefixIndex = 0 return WrappedSequence(wrapping: self) { iterator in
上面,我们使用一个参数,该参数具有可变数量的参数(在类型上添加...),以允许将一个或多个元素传输到同一方法。
以相同的方式,我们可以创建一种将给定后缀集添加到序列末尾的方法-首先执行我们自己的基本序列迭代,然后遍历带后缀的元素:
extension Sequence { func suffixed( with suffixElements: Element... ) -> WrappedSequence<Self, Element> { var suffixIndex = 0 return WrappedSequence(wrapping: self) { iterator in guard let next = iterator.next() else {
通过上面提到的两种方法,我们现在可以将前缀和后缀添加到所需的任何序列中。 以下是一些有关如何使用我们的新API的示例:
尽管上述所有示例都可以使用特定类型(例如Array和String)实现,但使用WrappedSequence类型的优点是可以懒惰地完成所有操作-我们不执行任何突变或评估任何序列来添加我们的代码前缀或后缀-在对性能至关重要的情况下或使用大型数据集时,这很有用。
细分
接下来,让我们看看如何包装序列以创建它们的分段版本。 在某些迭代中,仅了解当前元素是不够的-我们可能还需要有关下一个和上一个元素的信息。
当使用索引序列时,我们通常可以使用enumerated()API来实现这一点,该API还使用序列包装器使我们能够访问当前元素及其索引:
for (index, current) in list.items.enumerated() { let previous = (index > 0) ? list.items[index - 1] : nil let next = (index < list.items.count - 1) ? list.items[index + 1] : nil ... }
但是,上述技术不仅在调用方面非常冗长,而且还依赖于再次使用数组-或至少某种形式的序列使我们可以随机访问其元素-许多序列,尤其是惰性序列,不欢迎。
取而代之的是,让我们再次使用WrappedSequence -创建一个序列包装器,以延迟的方式在其基本序列中提供分段视图,跟踪先前和当前的元素,并在继续迭代时对其进行更新:
extension Sequence { typealias Segment = ( previous: Element?, current: Element, next: Element? ) var segmented: WrappedSequence<Self, Segment> { var previous: Element? var current: Element? var endReached = false return WrappedSequence(wrapping: self) { iterator in
现在,只要需要在进行迭代时向前或向后看,就可以使用上述API创建任何序列的分段版本。 例如,以下是我们如何使用细分的方法,以便我们可以轻松确定何时到达列表末尾:
for segment in list.items.segmented { addTopBorder() addView(for: segment.current) if segment.next == nil { // , addBottomBorder() } } ```swift , . , : ```swift for segment in path.nodes.segmented { let directions = ( enter: segment.previous?.direction(to: segment.current), exit: segment.next.map(segment.current.direction) ) let nodeView = NodeView(directions: directions) nodeView.center = segment.current.position.cgPoint view.addSubview(nodeView) }
现在,我们开始看到包装序列的真正威力-通过它们,我们可以在真正简单的API中隐藏越来越多的复杂算法。 调用者需要对序列进行分段,只需访问任何Sequence中的segmented属性即可,其余的工作将由我们的基本实现完成。
递归
最后,让我们看一下如何使用序列包装器对递归迭代进行建模。 假设我们想提供一种简单的方法来递归地迭代值的层次结构,其中层次结构中的每个元素都包含一系列子元素。 正确地做到这一点可能非常困难,因此,如果我们可以使用一种实现在代码库中执行所有此类迭代,那将是很好的。
使用WrappedSequence,我们可以通过使用一种使用相同泛型类型约束的方法来扩展Sequence来确保每个元素都可以提供与原始迭代器类型相同的嵌套序列,从而实现此目的。 为了能够动态访问每个嵌套序列,我们还将要求调用方为应用于递归的属性指定KeyPath,这将为我们提供一个类似于以下内容的实现:
extension Sequence { func recursive<S: Sequence>( for keyPath: KeyPath<Element, S> ) -> WrappedSequence<Self, Element> where S.Iterator == Iterator { var parentIterators = [Iterator]() func moveUp() -> (iterator: Iterator, element: Element)? { guard !parentIterators.isEmpty else { return nil } var iterator = parentIterators.removeLast() guard let element = iterator.next() else {
使用上面的代码,我们现在可以递归地遍历任何序列,而不管它是如何在内部构建的,而不必事先加载整个层次结构。 例如,以下是我们如何使用此新API递归遍历文件夹层次结构的方法:
let allFolders = folder.subfolders.recursive(for: \.subfolders) for folder in allFolders { try loadContent(from: folder) }
我们还可以使用它遍历树的所有节点或递归遍历一组数据库记录,例如,列出组织中的所有用户组:
let allNodes = tree.recursive(for: \.children) let allGroups = database.groups.recusive(for: \.subgroups)
关于递归迭代,我们需要注意的一件事是防止循环引用(当某个路径将我们返回到我们已经遇到的元素时),这将导致我们陷入无限循环。
解决此问题的一种方法是跟踪所有发生的元素(但是从内存的角度来看可能会出现问题),确保我们的数据集中没有循环引用,或者每次从调用方处理此类情况(使用break,continue或return来完成所有操作)循环迭代)。
结论
序列是标准库中最简单的协议之一-它仅需要一种方法-但它仍然是最强大的协议之一,尤其是涉及到我们可以基于它创建多少功能时。 正如标准库包含枚举之类的包装器序列一样,我们也可以创建自己的包装器-允许我们使用非常简单的API隐藏高级功能。
尽管抽象总是要付出代价的,但重要的是要考虑何时应该引入它们(也许更重要的是何时不值得引入),如果我们可以直接在标准库提供的东西之上(使用相同的约定)构建抽象,那么这些抽象通常更容易经受时间的考验。