如果有这样的一个需求,我希望能像数组一样,用 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)"
}
}
复制代码

迭代器中的 children 是 AnyCollection<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,这里我就不给出自己的实现,同学们可以自己动手实现一下。

写在最后

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

https://juejin.im/post/5b416e25e51d4519730775c8

从 Swift 中的序列到类型擦除的更多相关文章

  1. [ios][swift]swift中如果做基本类型的转换

    在swift中如果做基本类型的转换的?比如Int -> Float(Double)Double -> 保留两位小数String -> IntDouble -> String 有 ...

  2. Swift中如何转换不同类型的Mutable指针

    在Swift中我们拥有强大高级逻辑抽象能力的同时,低级底层操作被刻意的限制了.但是有些情况下我们仍然想做一些在C语言中的hack工作,下面本猫就带大家看一看如何做这样的事. hacking is ha ...

  3. Swift基础--通知,代理和block的使用抉择以及Swift中的代理

    什么时候用通知,什么时候用代理,什么时候用block 通知 : 两者关系层次太深,八竿子打不着的那种最适合用通知.因为层级结构深了,用代理要一层一层往下传递,代码结构就复杂了 代理 : 父子关系,监听 ...

  4. swift 中Value Type VS Class Type

    ios 中Value Type 和 Class Type 有哪些异同点,这个问题是在微信的公共帐号中看到的,觉得挺有意思,这里梳理一下. 1.swift 中为什么要设置值类型? 值类型在参数传递.赋值 ...

  5. 深圳尚学堂:Swift中的“!”和“?”

    Swift中的"!"和"?"Swift,苹果于2014年WWDC发布的新开发语言,用于搭建基于苹果平台的应用程序.Swift是一款易学易用的编程语言,而且它还是 ...

  6. 初识Swift中的值和引用,循坏引用、代理的注意点

    1.0 在Swift中分有值类型和引用类型 Int .String . 结构体和枚举都属于值类型, 将值类型传递给方法是,将在内存中创建其副本,并传递这个副本:这样我们就可以随心所欲修改它,而不用担心 ...

  7. swift中Any,AnyObject,AnyClass的区别

    这几个概念让人很迷惑,看了很多帖子,终于搞明白了,简单总结: Any 和 AnyObject 是 Swift 中两个妥协的产物.什么意思呢,oc中有个id关键字,表示任何对象,oc和swift混编的时 ...

  8. Swift中的Any 与 AnyObject、AnyClass的区别?

    在 Swift 中能够表示 “任意” 这个概念的除了Any .AnyObject以外,还有一个AnyClass. Any.AnyObject.AnyClass有什么区别: AnyObject是一个成员 ...

  9. Type Erasure with Pokemon---swift的类型擦除

    我感觉这个是swift的设计缺陷. 类型擦除:解决泛型类型作为公用类型的问题 是抽象的公用机制的一种实现方式. 1)类型擦除并不能解决类型不一致的兼容问题,只能解决类似继承一致性的兼容问题. 2)擦除 ...

随机推荐

  1. [编程] C语言的结构体

    结构体 struct 结构体名{} 变量名; 结构体变量: struct person{ char *name; int age; float score; } student; 成员的获取和赋值 / ...

  2. hdu 3415 Max Sum of Max-K-sub-sequence 单调队列。

    Max Sum of Max-K-sub-sequence Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K ...

  3. fuzhou 1692 Key problem ***

    Problem 1692 Key problem Accept: 103    Submit: 553 Time Limit: 1000 mSec    Memory Limit : 32768 KB ...

  4. Q:接口与抽象类

    博文回答一下两个问题: 接口和抽象类的区别 选用接口和抽象类的依据 对于问题1: 从java语法的角度上来说,接口的所有成员和方法都是public的,且其方法均为abstract的.直到jdk1.8之 ...

  5. JavaWeb学习总结(五):HttpServletRespone对象(一)

    Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象.和代表响应的response对象.request和response对象即然代表请求和响应,那我们要 ...

  6. 【数据库】java链接jdbc 释放资源

    /* *   编写快速 入门的jdbc 程序 : *    *    1. 先导入 具体的驱动jar包 *    2. 编写一个类 , 写jdbc 的程序 *  *  具体的编写 java类的 代码的 ...

  7. SpringBoot 整合 Mybatis + Mysql——XML配置方式

    一.介绍 SpringBoot有两种方法与数据库建立连接,一种是集成Mybatis,另一种用JdbcTemplate,本文主要讨论集成Mybatis方式. SpringBoot整合Mybatis也有两 ...

  8. Ubuntu 17.04 upgrade to 17.10

    Just try  sudo do-release-upgrade if you get this An upgrade from 'zesty' to 'bionic' is not support ...

  9. Recovering InnoDB table from an .ibd file.

    Recovering an InnoDB table from only an .ibd file. Sometime you may need to recover a table when all ...

  10. MySQL 练习题2

    CREATE TABLE `dept` ( `did` ) NOT NULL AUTO_INCREMENT, `dname` ) DEFAULT NULL, `address` ) DEFAULT N ...