Swift标准库源码之旅 - SequenceAlgorithms.swift

SequenceAlgorithms.swift

EnumeratedSequence

每一个编程语言对集合类型的遍历都有获取对应遍历索引的需求.

Swift中对序列(集合)类型遍历如果想拿到对应索引需要调用一个enumerated方法


var s = ["foo", "bar"].enumerated()
for (n, x) in s {
    print("\(n): \(x)")
}
// Prints "0: foo"
// Prints "1: bar"

因为单纯的sequence只能迭代遍历自身元素,想要遍历时的索引,Swift的做法是先转换为一个新序列来增加这个功能,很符合每一种功能和约束都降到最小的原则。这个enumerated()方法返回一个新序列.

public func enumerated() -> EnumeratedSequence<Self> {
    return EnumeratedSequence(_base: self)
}

struct EnumeratedSequence<Base: Sequence> {
    
    var base: Base
    init(_ base: Base) {
        self.base = base
    }
}

DropFirstSequence等相似,这个sequence引用了一份之前的sequence,修改了一下迭代方法而已

extension EnumeratedSequence {
    struct Iterator {
        var base: Base.Iterator
        var count: Int
        
        init(base: Base.Iterator) {
            self.base = base
            self.count = 0
        }
    }
}

extension EnumeratedSequence.Iterator: IteratorProtocol {
        
    typealias Element = (offset: Int, element: Base.Element)
    mutating func next() -> Element? {
        guard let b = base.next() else { return nil }
        defer {
            count += 1
        }
        return (count, b)
    }
}

新的迭代器方法也是通过之前的sequence的迭代器构建的

extension MyEnumeratedSequence: MySequence {
    typealias Element = Iterator.Element
    
    func makeIterator() -> Iterator {
        return Iterator(base: base.makeIterator())
    }
}

只不过增加了一个count属性用来在迭代的时候自增,并且这个新序列的元素是一个索引和元素的元组,这样就可以用来迭代索引了。

但是索引这个词在这里这么说其实是不恰当的,因为这里我们只是一个Int数值的增加,对于有些集合类型的索引其实并不是Int类型,例如String,用这个序列的迭代时的Int值也取不到对应的第n个元素,这里不展开讨论,正如元组的命名一样这个int是一种offset。嗯。。

Min & Max 函数

func min(_ predicate: (Element, Element) -> Bool) -> Element? {
    var it = makeIterator()
    guard var result = it.next() else { return nil }
    while let e = it.next() {
        if predicate(e, result) { result = e }
    }
    return result
}

第一次见这个函数我是懵的,为什么min函数会有一个predicate函数,而且还让我比较这两个参数返回个Bool值. 那我岂不是可以随便返回bool值, 比较的事情不应该是min函数帮我做好的,我只要min()就可以了吗?

当我们用Array<Int>类型时它确实有一个min()签名的函数,但我们目前是在sequence的扩展函数里,Element也并没有限定是Equalble,所以这个通用的Min函数给了我们目前的元素和下一个元素让我们告诉它元素该如何比较。

对于Array<Int>要找其中的元素确实很简单也不需要我们指定改如何比,所以有了min()的版本,但是如何数组中元素是我们自定义的Model类型. 比如

struct IntBox {
    let num: Int
}

你如果想找出一个Array<IntBox>中的最小值你通常应该会这样做了

Array<IntBox>.map { $0.num }.min()

但是这样做的话实际上是遍历了两遍数组,我们如果使用上面通用的Min函数可以只遍历一遍,降低一些复杂度

Array<IntBox>.min { $0.num < $1.num }

但是min函数中间用<还是max函数中间用>...说实话我应该找个什么方式记忆一下

所以.. 大部分时候还是依赖于下面这种扩展

extension Sequence where Element: Comparable {

  func min() -> Element? {
    return self.min(by: <)
  }
  
  func max() -> Element? {
    return self.max(by: <)
  }
}

starts(possiblePrefix)

用来判断两个序列,另一个是否是第一个的子序列(从头开始的那种)
判断方式就是遍历从第一个元素开始比较两个序列的元素

func starts<PossiblePrefix: Sequence>(
    with possiblePrefix: PossiblePrefix,
    by areEquivalent: (Element, PossiblePrefix.Element) -> Bool
  ) -> Bool {
    var possiblePrefixIterator = possiblePrefix.makeIterator()
    for e0 in self {
      if let e1 = possiblePrefixIterator.next() {
        if !areEquivalent(e0, e1) {
          return false
        }
      }
      else {
        return true
      }
    }
    return possiblePrefixIterator.next() == nil
  }
}

在我想自己实现这个函数的时候我没有用for循环而是打算用forEach进行迭代,结果发现在forEach函数里想要return true作为这个stats函数的返回值是不可行的,因为forEach里的代码都是在(Element) -> Void这个函数范围里,return的作用域也在这里面,编译器因此会报错Unexpected non-void return value in void function

这一点并不能算是forEach的缺陷,这种函数本身意义就在于对一个序列元素迭代做某些操作,它会全部遍历完,如果中途还想停止的话本身就不应该考虑使用这个函数了。

这个starts函数还有一个方便的版本

extension Sequence where Element: Equatable {

  func starts<PossiblePrefix: Sequence>(
    with possiblePrefix: PossiblePrefix
  ) -> Bool where PossiblePrefix.Element == Element {
    return self.starts(with: possiblePrefix, by: ==)
  }
}

像上面的minmax函数一样,大部分时候我们都会使用Element: Equatable的版本,但是Swift会提供一个所有元素都可以用的版本让我们自己指定判断逻辑,更加灵活,有时也会免于做一次复杂度O(n)的map操作让元素变为Equatable

elementsEqual

用来比较两个序列是否完全一样


func elementsEqual<OtherSequence: MySequence>(
    _ other: OtherSequence,
    by areEquivalent: (Element, OtherSequence.Element) -> Bool) -> Bool {
    var it1 = makeIterator()
    var it2 = other.makeIterator()
    while true {
        switch (it1.next(), it2.next()) {
        case let (e1?, e2?):
            if !areEquivalent(e1, e2) {
                return false
            }
        case (_?, nil), (nil, _?):
            return false
        case (nil, nil):
            return true
        }
    }
}
    

这个while true 和对Optional的Switch用法还是可以学一下的..

能用这个方法的两个序列同样需要是有限的,并且迭代不要产生什么副作用,这一点使用者应该了解

它同样有一个Element为Comparable的版本..

contains

这个是比较常用没有什么好说的同样方法了

func contains(where predicate: (Element) -> Bool) -> Bool {
    var it = makeIterator()
    while let e = it.next() {
        if predicate(e) {
            return true
        }
    }
    return false
}
    
func allSatisfy(_ predicate: (Element) -> Bool) -> Bool {
    return !contains { !predicate($0) }
}

extension MySequence where Element: Equatable {
    func contains(_ element: Element) -> Bool {
        return self.contains(where: { $0 == element })
    }
}

reduce

extension MySequence {
    func reduce<Result>(initalize: Result, nextPartialResult: (Result, Element) -> Result) -> Result {
        var result = initalize
        var it = makeIterator()
        while let e = it.next() {
            result = nextPartialResult(result, e)
        }
        return result
    }
}

想要转换一个序列最灵活的函数,把迭代的当前数值和上一个数值给你去返回一个新值来进行转换,转换的类型为一个泛型,意味着你也可以把一个数组转换为Int或者String. 也可以用它实现map或者compactMap

我们用reduce实现一下filter函数吧

func reduceFilter(_ predicate: (Element) -> Bool) -> [Element] {
    return self.reduce(initalize: []) { (last, new) -> [Element] in
        var elements = last
        if predicate(new) {
            elements.append(new)
        }
        return elements
    }
}

因为很灵活,所以这个函数签名还是挺复杂的,一个初始值,predicate是两个参数一个返回值,函数本身还有一个返回值。要根据实际情况使用,个人觉得用的多了会增加一些阅读负担

reduce还有一个版本,闭包参数并不是返回一个新值来代表这次的变换,而是直接将上一次的值加了inout参数来让我们修改,作为这次变换的结果,这其实才是大部分情况的场景,不然参数是没办法直接修改,我们还需要用个变量接住上一次的值再对其进行变更,这个版本是这样的

func reduce<Result>(into: Result, _ updateAccumulatingResult: (inout Result, Element) -> Void) -> Result {
    var accumulator = into
    var it = makeIterator()
    while let e = it.next() {
        updateAccumulatingResult(&accumulator, e)
    }
    return accumulator
}

flatMap/compactMap/reversed 不展开说了

就是Swift4.0之前是没有compactMap的,使用两个不一样的flatMap签名函数来实现的这两个功能,现在将其中一个分出来了compactMap. 这么做的好处也是很明显的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容