Swift標(biāo)準(zhǔn)庫源碼之旅 - LazySequence

背景

Lazy sequences can be used to avoid needless storage allocation and computation, because they use an underlying sequence for storage and compute their elements on demand.

在使用數(shù)組或某一序列的map/filter等方法的時(shí)候,容器會(huì)立刻進(jìn)行遍歷對(duì)所有元素執(zhí)行transformisIncluded閉包竹挡,時(shí)間復(fù)雜度是O(n)镀娶,并且返回的新數(shù)組也會(huì)占用對(duì)應(yīng)內(nèi)存空間。如果transform里的操作比較耗時(shí)揪罕,容器元素比較多梯码,那么此種方式的時(shí)間消耗就不能忽略了。

舉個(gè)極端的例子:

       let result1 = Array(repeating: 3, count: 99).map { (num) -> String in
            (0 ... 9999).reduce("0") { (current, num) in
                return current + num.description
            }
        }

上述代碼的執(zhí)行時(shí)間需要5.8s. 即使我們還根本沒有用到其中的元素.

Lazy的方式就是將transform等需要遍歷元素的操作進(jìn)行延時(shí)好啰,在需要元素的時(shí)候再進(jìn)行transform.

思路

transform閉包作為屬性保存下來轩娶,重寫迭代方法,在迭代之前元素的基礎(chǔ)上進(jìn)行·transform·返回框往。

實(shí)現(xiàn) LazyMapSequence

所有的Sequence皆可進(jìn)行lazy. 為Sequence類型添加擴(kuò)展方法

extension Sequence {
 var lazy: LazySequence<Self> {
    return LazySequence(_base: self)
  }
}

LazySequence是一個(gè)中間類型鳄抒,把原始Sequence作為屬性保存起來備用.后面的map/filter都在這個(gè)類型基礎(chǔ)上擴(kuò)展

struct LazySequence<Base: Sequence>: Sequence {
    typealias Element = Base.Element
    
    var _base: Base
    init(_ base: Base) {
        self._base = base
    }
    
    func makeIterator() -> Base.Iterator {
        return _base.makeIterator()
    }
}

LazySequence提供map方法,返回值是一個(gè)重寫了迭代方法的新的Sequence

extension LazySequence {
    func map<ResultElement>(_ transform: @escaping (Element) -> ResultElement) -> LazyMapSequence<Base, ResultElement> {
        LazyMapSequence(self._base, transform: transform)
    }
}

他要把transform和原始序列保存下來.

struct LazyMapSequence<Base: Sequence, Element>: Sequence {
    
    typealias Element = Element
        
    var _base: Base
    var transform: (Base.Element) -> Element
    
    init(_ base: Base, transform: @escaping (Base.Element) -> Element) {
        self._base = base
        self.transform = transform
    }
    
    func makeIterator() -> Iterator {
        Iterator(_base.makeIterator(), transform: transform)
    }
}

他的迭代方法如下,取base的元素然后進(jìn)行transform返回

extension LazyMapSequence {
    struct Iterator: MyIteratorProtocol {
        var _base: Base.Iterator
        var transform: (Base.Element) -> Element
        
        init(_ base: Base.Iterator, transform: @escaping (Base.Element) -> Element) {
            self._base = base
            self.transform = transform
        }
        
        mutating func next() -> Element? {
            return _base.next().map(transform)
        }
    }
}

以上僅解決了Sequence進(jìn)行map延遲遍歷的問題.
更常用的情況是一個(gè)數(shù)組map后使用下標(biāo)取值.也需要支持.

也就是當(dāng)LazyMapSequenceBase是個(gè)Collection的時(shí)候

public typealias LazyMapCollection<T: Collection,U> = LazyMapSequence<T,U>

然后就可以給LazyMapCollection類型擴(kuò)展方法了

extension LazyMapCollection: Collection {
  ...
  ...
  subscript(position: Base.Index) -> Element {
    return _transform(_base[position])
  }
}

這樣,數(shù)組類型的lazy后的map方法返回的LazyMapSequence就支持了下標(biāo)訪問.transform只在取某一值的時(shí)候調(diào)用.

上面只完成了map方法的lazy. 對(duì)于Filter方法想要實(shí)現(xiàn)lazy同樣需要已基本相同的方式寫一個(gè)LazyFilterSequence寫其對(duì)應(yīng)的迭代方法.

## 實(shí)現(xiàn) LazyFilterSequence

extension LazySequence {
    func filter(_ isIncluded: @escaping (Element) -> Bool) -> LazyFilterSequence<Base> {
        LazyFilterSequence(_base, isIncluded: isIncluded)
    }
}

struct LazyFilterSequence<Base: MySequence>: MySequence {
    
    typealias Element = Base.Element
        
    var _base: Base
    var _predicate: (Base.Element) -> Bool
    
    init(_ base: Base, isIncluded: @escaping (Base.Element) -> Bool) {
        self._base = base
        self._predicate = isIncluded
    }
    
    func makeIterator() -> Iterator {
        Iterator(_base.makeIterator(), isIncluded: _predicate)
    }
}

extension LazyFilterSequence {
    struct Iterator: MyIteratorProtocol {
        var _base: Base.Iterator
        var _predicate: (Base.Element) -> Bool
        
        init(_ base: Base.Iterator, isIncluded: @escaping (Base.Element) -> Bool) {
            self._base = base
            self._predicate = isIncluded
        }
        
        mutating func next() -> Element? {
            while let value = _base.next() {
                if _predicate(value) {
                    return value
                }
            }
            return nil
        }
    }
}

LazyMapSequence實(shí)現(xiàn)相似椰弊,不在贅述许溅。

但是Filter是篩選元素得到一個(gè)新結(jié)果,數(shù)組的下標(biāo)訪問如何做到lazy呢? 不執(zhí)行完Filter是不能拿到最后數(shù)組個(gè)數(shù)的吧秉版。帶著這個(gè)猜想發(fā)現(xiàn)了有意思的情況.

        let result = Array(repeating: 100, count: 99999).lazy.filter { $0 > 100 }
        print(result.count) // 0
        print(result[3]) // 100

這個(gè)篩選結(jié)果必然是空的.count為0是沒問題的. 但是下標(biāo)取值[3]確是返回了100

去看一下源碼發(fā)現(xiàn)下標(biāo)取值的方法是取的原數(shù)組的值, 并沒有經(jīng)過篩選

subscript(position: Index) -> Element {
    return _base[position]
}

所以下面這種操作方式也會(huì)得到你意想不到的結(jié)果.

        var array: [String?] = ["a", nil, nil, "c"]
        let result = array.lazy.filter { $0 != nil }
        for i in 0 ..< result.count {
            print(result[i])   // a, nil
        }

result本來是篩選過的不為nil的結(jié)果,期望值應(yīng)該是"a"和"c". 結(jié)果卻是"a"和"nil"

有一些方法沒有新生成一個(gè)LazyxxSequence而是建立在上面兩個(gè)Sequence之上的.

Lazy的CompactMap

func compactMap<ElementOfResult>(
    _ transform: @escaping (Elements.Element) -> ElementOfResult?
  ) -> LazyMapSequence<
    LazyFilterSequence<
      LazyMapSequence<Elements, ElementOfResult?>>,
    ElementOfResult
  > {
    return self.map(transform).filter { $0 != nil }.map { $0! }
  }

它是由LazyFilterSequenceLazyMapSequence組合實(shí)現(xiàn)的方法.因?yàn)橛玫搅薋ilter. 下標(biāo)取值仍然是取得原Collection的值.
但是自己實(shí)現(xiàn)時(shí)候發(fā)現(xiàn)LazyMapSequence類型其實(shí)并沒有Filter方法, LazyFilterSequence方法也并沒有map方法. 手動(dòng)給具體類型加上嗎? 但后面還有LazyDropWhileSequence類型,想要自由組合的話這種方式太硬了一點(diǎn)..
讓他們共同遵循一個(gè)協(xié)議贤重,給此協(xié)議添加擴(kuò)展方法就可以讓所有類型都具有map/filter

protocol LazySequenceProtocol: MySequence {
    
}
extension LazySequenceProtocol {
    func filter(_ isIncluded: @escaping (Element) -> Bool) -> LazyFilterSequence<Self> {
        LazyFilterSequence(self, isIncluded: isIncluded)
    }
}
extension LazySequenceProtocol {
    func map<U>(_ transform: @escaping (Element) -> U) -> LazyMapSequence<Self, U> {
        .init(self, transform: transform)
    }
}

接下來就有了上面CompactMap返回的多個(gè)尖括號(hào)的泛型嵌套類型。好處是可以自由組合清焕。

為什么LazySequenceProtocol里沒有定義方法并蝗,其實(shí)Swift源代碼里是有的,但是筆者目前沒有弄明白里面方法的意義耐朴。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末借卧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子筛峭,更是在濱河造成了極大的恐慌铐刘,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件影晓,死亡現(xiàn)場離奇詭異镰吵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)挂签,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門疤祭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饵婆,你說我怎么就攤上這事勺馆。” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵草穆,是天一觀的道長灌灾。 經(jīng)常有香客問我,道長悲柱,這世上最難降的妖魔是什么锋喜? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮豌鸡,結(jié)果婚禮上嘿般,老公的妹妹穿的比我還像新娘。我一直安慰自己涯冠,他們只是感情好炉奴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛇更,像睡著了一般盆佣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上械荷,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音虑灰,去河邊找鬼吨瞎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛穆咐,可吹牛的內(nèi)容都是我干的颤诀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼对湃,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼崖叫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拍柒,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤心傀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拆讯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脂男,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年种呐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宰翅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爽室,死狀恐怖汁讼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤嘿架,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布瓶珊,位于F島的核電站,受9級(jí)特大地震影響眶明,放射性物質(zhì)發(fā)生泄漏艰毒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一搜囱、第九天 我趴在偏房一處隱蔽的房頂上張望丑瞧。 院中可真熱鬧,春花似錦蜀肘、人聲如沸绊汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽西乖。三九已至,卻和暖如春坛增,著一層夾襖步出監(jiān)牢的瞬間获雕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工收捣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留届案,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓罢艾,卻偏偏與公主長得像楣颠,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咐蚯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353