从 Swift 中的序列到类型擦除

从 Swift 中的序列到类型擦除_第1张图片
Swift.png

如果有这样的一个需求,我希望能像数组一样,用 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 的集合类型,每次迭代返回一个值后,更新 children 这个状态,这样我们的迭代器就可以持续的输出正确的值了,直到输出完 children 中的所有值。

现在可以使用 for 循环输出 Persion 中所有的属性值了:

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

// out put:
// name is jewelz
// age is 23
// email is [email protected]

如果现在有另外一个结构体或类也需要迭代输出所以属性呢?,这很好办,让我们的结构体遵守 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 {
    return _AnyIterator(iteratorImpl)
}

我们的这个封装可以这样定义:

struct _AnySequence: Sequence {
    private var iteratorImpl: () -> Element?
}

对于刚刚上面的那个数组就可以这样初始化了:

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

这里的 _AnySequence 就将具体的 Sequence 类型隐藏了,调用者只知道数组中的元素是一个可以迭代输出字符串类型的序列。

现在我们可以一步步来实现上面的 _AnyIterator 和 _AnySequence。_AnyIterator 的实现跟上面提到的 _AnySequence 的思路一致。我们不直接存储迭代器,而是让封装类存储迭代器的 next 函数。要做到这一点,我们必须首先将 iterator 参数复制到一个变量中,这样我们就可以调用它的 next 方法了。下面是具体实现:

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

extension _AnyIterator: IteratorProtocol {
    init(_ 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 [email protected]

我们希望外面传入一个闭包也能创建一个 _AnyIterator,现在我们添加下面的代码:

 init(_ impl: @escaping () -> Element?) {
     nextImpl = impl
 }

添加这个初始化方法其实为了方便后面实现 _AnySequence 用的。上面说过 _AnySequence 有个属性存储了迭代器的实现,所以我们的 _AnyIterator 能通过一个闭包来初始化。

_AnyIterator 实现完后就可以来实现我们的 _AnySequence 了。我这里直接给出代码,同学们可以自己去实现:


struct _AnySequence {
    typealias Iterator = _AnyIterator
    private var iteratorImpl: () -> Element?
}

extension _AnySequence: Sequence {
    init(_ base: S) where Element == S.Iterator.Element, S: Sequence {
        var iterator = base.makeIterator()
        iteratorImpl = {
            iterator.next()
        }
    }

     func makeIterator() -> _AnyIterator {
        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 [email protected]
// +-------------------------+
// name is Sequence
// author is Persion(name: "jewelz", age: 23, email: "[email protected]")

得益于 Swift 的类型推断,这里的 array 可以不用显式地指明其类型,点击 option 键,你会发现它是 [_AnySequence] 类型。也就是说只有其元素是 String 的任意序列都可以作为数组的元素。这就跟我们平时使用类似 "一个 Int 类型的数组" 的语义是一致的了。如果要向数组中插入一个新元素,可以这样创建一个序列:

let s = _AnySequence { () -> _AnyIterator in
    return _AnyIterator { () -> String? in
        return arc4random() % 10 == 5 ? nil : String(Int(arc4random() % 10))
    }
}

array.append(s)

上面的代码中通过一个闭包初始化了一个 _AnySequence,这里我就不给出自己的实现,同学们可以自己动手实现一下。

写在最后

在标准库中,其实已经提供了 AnyIteratorAnySequence。我还没去看标准库的实现,有兴趣的同学可以点击这里查看。 我这里实现了自己的 _AnyIterator 和 _AnySequence 就是为了提供一种实现类型擦除的思路。如果你在项目中频繁地使用带有关联类型或 Self 的协议,那么你也一定会遇到跟我一样的问题。这时候实现一个类型擦除的封装,将具体的类型隐藏了起来,你就不用为 Xcode 的报错而抓狂了。

你可能感兴趣的:(从 Swift 中的序列到类型擦除)