從 Swift 中的序列到型別擦除

胡蘿蔔卜發表於2019-02-27

如果有這樣的一個需求,我希望能像陣列一樣,用 for 迴圈遍歷一個類或結構體中的所有屬性。就像下面這樣:

let persion = Persion()
for i in persion {
    print(i)
}
複製程式碼

要實現這樣的需求,我們需要讓自定義的型別遵守 Sequence 協議。

序列

Sequence 協議是集合型別結構中的基礎。一個序列 (sequence) 代表的是一系列具有相同型別的值,你可以對這些值進行迭代。Sequence 協議提供了許多強大的功能,滿足該協議的型別都可以直接使用這些功能。上面這樣步進式的迭代元素的能力看起來十分簡單,但它卻是 Sequence 可以提供這些強大功能的基礎。

滿足 Sequence 協議的要求十分簡單,你需要做的所有事情就是提供一個返回迭代器 (iterator) 的 makeIterator() 方法:

public protocol Sequence {
    associatedtype Iterator : IteratorProtocol
    
    public func makeIterator() -> Self.Iterator
    
    // ...
}
複製程式碼

在 Sequence 協議有個關聯型別 Iterator,而且它必須遵守 IteratorProtocol 協議。從這裡我們可以看出 Sequence 是一個可以建立迭代器協議的型別。所以在搞清楚它的步進式的迭代元素能力之前,有必要了解一下迭代器是什麼。

迭代器

序列通過建立一個迭代器來提供對元素的訪問。迭代器每次產生一個序列的值,並且當遍歷序列時對遍歷狀態進行管理。在 IteratorProtocol 協議中唯一的一個方法是 next(),這個方法需要在每次被呼叫時返回序列中的下一個值。當序列被耗盡時,next() 應該返回 nil,不然迭代器就會一直工作下去,直到資源被耗盡為止。

IteratorProtocol 的定義非常簡單:

public protocol IteratorProtocol {
    associatedtype Element
    
    public mutating func next() -> Self.Element?
}
複製程式碼

關聯型別 Element 指定了迭代器產生的值的型別。這裡next() 被標記了 mutating,表明了迭代器是可以存在可變的狀態的。這裡的 mutating 也不是必須的,如果你的迭代器返回的值並沒有改變迭代器本身,那麼沒有 mutating 也是沒有任何問題的。 不過幾乎所有有意義的迭代器都會要求可變狀態,這樣它們才能夠管理在序列中的當前位置。

對 Sequence 和 IteratorProtocol 有了基礎瞭解後,要實現開頭提到的需求就很簡單了。比如我想迭代輸出一個 Person 例項的所有屬性,我們可以這樣做:

struct Persion: Sequence {
    var name: String
    var age: Int
    var email: String
    
    func makeIterator() -> MyIterator {
        return MyIterator(obj: self)
    }
}
複製程式碼

Persion 遵守了 Sequence 協議,並返回了一個自定義的迭代器。迭代器的實現也很簡單:

struct MyIterator: IteratorProtocol {
    var children: Mirror.Children
    
    init(obj: Persion) {
        children = Mirror(reflecting: obj).children
    }
   
    mutating func next() -> String? {
        guard let child = children.popFirst() else { return nil }
        return "(child.label.wrapped) is (child.value)"
    }
}
複製程式碼

迭代器中的 childrenAnyCollection<Mirror.Child> 的集合型別,每次迭代返回一個值後,更新 children 這個狀態,這樣我們的迭代器就可以持續的輸出正確的值了,直到輸出完 children 中的所有值。

現在可以使用 for 迴圈輸出 Persion 中所有的屬性值了:

for item in Persion.author {
    print(item)
}

// out put:
// name is jewelz
// age is 23
// email is hujewelz@gmail.com
複製程式碼

如果現在有另外一個結構體或類也需要迭代輸出所以屬性呢?,這很好辦,讓我們的結構體遵守 Sequence 協議,並返回一個我們自定義的迭代器就可以了。這種拷貝程式碼的方式確實能滿足需求,但是如果我們利用協議擴充就能寫出更易於維護的程式碼,類似下面這樣:

struct _Iterator: IteratorProtocol {
    var children: Mirror.Children
    
    init(obj: Any) {
        children = Mirror(reflecting: obj).children
    }
    
    mutating func next() -> String? {
        guard let child = children.popFirst() else { return nil }
        return "(child.label.wrapped) is (child.value)"
    }
}

protocol Sequencible: Sequence { }

extension Sequencible {
    func makeIterator() -> _Iterator {
        return _Iterator(obj: self)
    }
}
複製程式碼

這裡我定義了一個繼承 Sequence 的空協議,是為了不影響 Sequence 的預設行為。現在只要我們自定義的類或結構體遵守 Sequencible 就能使用 for 迴圈輸出其所有屬性值了。就像下面這樣:

struct Demo: Sequencible {
    var name = "Sequence"
    var author = Persion.author
}
複製程式碼

表示相同序列的型別

現在需求又變了,我想將所有遵守了 Sequencible 協議的任何序列存到一個陣列中,然後 for 迴圈遍歷陣列中的元素,因為陣列中的元素都遵守了 Sequencible 協議,所以又可以使用 for 迴圈輸出其所有屬性,就像下面這樣:

for obj in array {
    for item in obj {
        print(item)
    }
}
複製程式碼

那麼這裡的 array 應該定義成什麼型別呢?定義成 [Any] 型別肯定是不行的,這樣的話在迴圈中得將 item 強轉為 Sequencible,那麼是否可以定義成 [Sequencible] 型別呢?答案是否定的。當這樣定義時編輯器會報出這樣的錯誤:

Protocol `Sequencible` can only be used as a generic constraint because it has Self or associated type requirements
複製程式碼

熟悉 Swift 協議的同學應該對這個報錯比較熟了。就是說含有 Self 或者關聯型別的協議,只能被當作泛型約束使用。所以像下面這樣定義我們的 array 是行不通的。

let sequencibleStore: [Sequencible] = [Persion.author, Demo()]
複製程式碼

如果有這樣一個型別,可以隱藏 Sequencible 這個具體的型別不就解決這個問題了嗎?這種將指定型別移除的過程,就被稱為型別擦除。

型別擦除

回想一下 Sequence 協議的內容,我們只要通過 makeIterator() 返回一個迭代器就可以了。那麼我們可以實現一個封裝類(結構體也是一樣的),裡面用一個屬性儲存了迭代器的實現,然後在 makeIterator() 方法中通過儲存的這個屬性構造一個迭代器。類似這樣:

func makeIterator() -> _AnyIterator<Element> {
    return _AnyIterator(iteratorImpl)
}
複製程式碼

我們的這個封裝可以這樣定義:

struct _AnySequence<Element>: Sequence {
    private var iteratorImpl: () -> Element?
}
複製程式碼

對於剛剛上面的那個陣列就可以這樣初始化了:

let sequencibleStore: [_AnySequence<String>] = [_AnySequence(Persion.author), _AnySequence(Demo())]
複製程式碼

這裡的 _AnySequence 就將具體的 Sequence 型別隱藏了,呼叫者只知道陣列中的元素是一個可以迭代輸出字串型別的序列。

現在我們可以一步步來實現上面的 _AnyIterator 和 _AnySequence。_AnyIterator 的實現跟上面提到的 _AnySequence 的思路一致。我們不直接儲存迭代器,而是讓封裝類儲存迭代器的 next 函式。要做到這一點,我們必須首先將 iterator 引數複製到一個變數中,這樣我們就可以呼叫它的 next 方法了。下面是具體實現:

struct _AnyIterator<Element> {
    var nextImpl: () -> Element?
}

extension _AnyIterator: IteratorProtocol {
    init<I>(_ iterator: I) where Element == I.Element, I: IteratorProtocol {
        var mutatedIterator = iterator
        nextImpl = { mutatedIterator.next() }
    }
    
    mutating func next() -> Element? {
        return nextImpl()
    }
}
複製程式碼

現在,在 _AnyIterator 中,迭代器的具體型別(比如上面用到的_Iterator)只有在建立例項的時候被指定。在那之後具體的型別就被隱藏了起來。我們可以使用任意型別的迭代器來建立 _AnyIterator 例項:

var iterator = _AnyIterator(_Iterator(obj: Persion.author))
while let item = iterator.next() {
    print(item)
}
// out put:
// name is jewelz
// age is 23
// email is hujewelz@gmail.com
複製程式碼

我們希望外面傳入一個閉包也能建立一個 _AnyIterator,現在我們新增下面的程式碼:

 init(_ impl: @escaping () -> Element?) {
     nextImpl = impl
 }
複製程式碼

新增這個初始化方法其實為了方便後面實現 _AnySequence 用的。上面說過 _AnySequence 有個屬性儲存了迭代器的實現,所以我們的 _AnyIterator 能通過一個閉包來初始化。

_AnyIterator 實現完後就可以來實現我們的 _AnySequence 了。我這裡直接給出程式碼,同學們可以自己去實現:

struct _AnySequence<Element> {

    typealias Iterator = _AnyIterator<Element>
    
    private var iteratorImpl: () -> Element?
}

extension _AnySequence: Sequence {
    init<S>(_ base: S) where Element == S.Iterator.Element, S: Sequence {
        var iterator = base.makeIterator()
        iteratorImpl = {
            iterator.next()
        }
    }
    
    func makeIterator() -> _AnyIterator<Element> {
        return _AnyIterator(iteratorImpl)
    }
}
複製程式碼

_AnySequence 的指定構造器也被定義為泛型,接受一個遵循 Sequence 協議的任何序列作為引數,並且規定了這個序列的迭代器的 next() 的返回型別要跟我們定義的這個泛型結構的 Element 型別要一致。這裡的這個泛型約束其實就是我們實現型別擦除的魔法所在了。它將具體的序列的型別隱藏了起來,只要序列中的值都是相同的型別就可以當做同一種型別來使用。就像下面的例子中的 array 就可以描述為 “元素型別是 String 的任意序列的集合”。

let array = [_AnySequence(Persion.author), _AnySequence(Demo())]

for obj in array {
    print("+-------------------------+")
    for item in obj {
        print(item)
    }
}
// out put:
// name is jewelz
//  age is 23
// email is hujewelz@gmail.com
// +-------------------------+
// name is Sequence
// author is Persion(name: "jewelz", age: 23, email: "hujewelz@gmail.com")
複製程式碼

得益於 Swift 的型別推斷,這裡的 array 可以不用顯式地指明其型別,點選 option 鍵,你會發現它是 [_AnySequence<String>] 型別。也就是說只有其元素是 String 的任意序列都可以作為陣列的元素。這就跟我們平時使用類似 “一個 Int 型別的陣列” 的語義是一致的了。如果要向陣列中插入一個新元素,可以這樣建立一個序列:

let s = _AnySequence { () -> _AnyIterator<String> in
    return _AnyIterator { () -> String? in
        return arc4random() % 10 == 5 ? nil : String(Int(arc4random() % 10))
    }
}
array.append(s)
複製程式碼

上面的程式碼中通過一個閉包初始化了一個 _AnySequence,這裡我就不給出自己的實現,同學們可以自己動手實現一下。

寫在最後

在標準庫中,其實已經提供了 AnyIteratorAnySequence。我還沒去看標準庫的實現,有興趣的同學可以點選這裡檢視。 我這裡實現了自己的 _AnyIterator 和 _AnySequence 就是為了提供一種實現型別擦除的思路。如果你在專案中頻繁地使用帶有關聯型別或 Self 的協議,那麼你也一定會遇到跟我一樣的問題。這時候實現一個型別擦除的封裝,將具體的型別隱藏了起來,你就不用為 Xcode 的報錯而抓狂了。

相關文章