Swift如何使用类型擦除及自定义详解

 更新时间:2018年10月12日 09:23:26   作者:WwiftGG  
有很多地方会用到类型擦除,并且它们的作用的各不相同。下面这篇文章主要给大家介绍了关于Swift如何使用类型擦除及自定义的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下

前言

在 Swift 的世界中,如果我们将协议称之为国王,那么泛型则可以视作皇后,所谓一山不容二虎,当我们把这两者结合起来使用的时候,似乎会遇到极大的困难。那么是否有一种方法,能够将这两个概念结合在一起,以便让它们成为我们前进道路上的垫脚石,而不是碍手碍脚的呢?答案是有的,这里我们将会使用到类型擦除 (Type Erasure) 这个强大的特性。

你也许曾听过类型擦除,甚至也使用过标准库提供的类型擦除类型如 AnySequence。但到底什么是类型擦除? 如何自定义类型擦除? 在这篇文章中,我将讨论如何使用类型擦除以及如何自定义。在此感谢 Lorenzo Boaro 提出这个主题。

有时你想对外部调用者隐藏某个类的具体类型,或是一些实现细节。在一些情况下,这样做能防止静态类型在项目中滥用,或者保证了类型间的交互。类型擦除就是移除某个类的具体类型使其变得更通用的过程。

协议或抽象父类可作为类型擦除简单的实现方式之一。例如 NSString 就是一个例子,每次创建一个 NSString 实例时,这个对象并不是一个普通的 NSString 对象,它通常是某个具体的子类的实例,这个子类一般是私有的,同时这些细节通常是被隐藏起来的。你可以使用子类提供的功能而不用知道它具体的类型,你也没必要将你的代码与它们的具体类型联系起来。

在处理 Swift 泛型以及关联类型协议的时候,可能需要使用一些高级的内容。Swift 不允许把协议当做具体的类型来使用。例如,如果你想编写一个方法,它的参数是一个包含了 Int 的序列,那么下面这种做法是不正确的:

func f(seq: Sequence<Int>) { ...

你不能这样使用协议类型,这样会在编译时报错。但你可以使用泛型来替代协议,解决这个问题:

func f<S: Sequence>(seq: S) where S.Element == Int { ...

有时候这样写完全可以,但有些地方还存在一些比较麻烦的情况,通常你不可能只在一个地方添加泛型: 一个泛型函数对其他泛型要求更多… 更糟糕的是,你不能将泛型作为返回值或者属性。这就跟我们想的有点不一样了。

func g<S: Sequence>() -> S where S.Element == Int { ...

我们希望函数 g 能返回任何符合的类型,但上面这个不同,它允许调用者选择他所需要的类型,然后函数 g 来提供一个合适的值。

Swift 标准库中提供了 AnySequence 来帮助我们解决这个问题。AnySequence 包装了一个任意类型的序列,并擦除了它的类型。使用 AnySequence 来访问这个序列,我们来重写一下函数 f 与 函数 g:

func f(seq: AnySequence<Int>) { ...

func g() -> AnySequence<Int> { ...

泛型部分不见了,同时具体的类型也被隐藏起来了。由于使用了 AnySequence 包装具体的值,它带来了一定的代码复杂性以及运行时间成本。但是代码却更简洁了。

Swift 标准库中提供了很多这样的类型,如 AnyCollection、AnyHashable 及 AnyIndex。这些类型在你自定义泛型或协议的时候非常的管用,你也可以直接使用这些类型来简化你的代码。接下来让我们探索实现类型擦除的多种方式吧。

基于类的类型擦除

有时我们需要在不暴露类型信息的情况下从多个类型中包装一些公共的功能,这听起来就像是父类-子类的关系。事实上我们的确可以使用抽象父类来实现类型擦除。父类提供 API 接口,不用去管谁来实现。而子类根据具体的类型信息实现相应的功能。

接下来我们将使用这种方式来自定义 AnySequence,我们将其命名为 MAnySequence:

class MAnySequence<Element>: Sequence {

这个类需要一个 iterator 类型作为 makeIterator 返回类型。我们必须要做两次类型擦除来隐藏底层的序列类型以及迭代器的类型。我们在 MAnySequence 内部定义了一个 Iterator 类,该类遵循着 IteratorProtocol 协议,并在 next() 方法中使用 fatalError 抛出异常。Swift 本身不支持抽象类型,但这样也够了:

class Iterator: IteratorProtocol {
 func next() -> Element? {
 fatalError("Must override next()")
 }
}

MAnySequence 对 makeIterator 方法实现也差不多。直接调用将抛出异常,这用来提示子类需要重写这个方法:

 func makeIterator() -> Iterator {
 fatalError("Must override makeIterator()")
 }
}

这样就定义了一个基于类的类型擦除的API,私有的子类将来实现这些API。公共类通过元素类型参数化,但私有实现类由它包装的序列类型进行参数化:

private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {

MAnySequenceImpl 需要一个继承于 Iterator 的子类:

class IteratorImpl: Iterator {

IteratorImpl 包装了序列的迭代器:

var wrapped: Seq.Iterator

init(_ wrapped: Seq.Iterator) {
 self.wrapped = wrapped
}

在 next 方法中调用被包装的序列迭代器:

 override func next() -> Seq.Element? {
 return wrapped.next()
 }
}

相似地,MAnySequenceImpl 包装一个序列:

var seq: Seq

init(_ seq: Seq) {
 self.seq = seq
}

从序列中获取迭代器,然后将迭代器包装成 IteratorImpl 对象返回,这样就实现了 makeIterator 的功能。

 override func makeIterator() -> IteratorImpl {
 return IteratorImpl(seq.makeIterator())
 }

}

我们需要一种方法来实际创建这些东西:对 MAnySequence 添加一个静态方法,该方法创建一个 MAnySequenceImpl 实例,并将其作为 MAnySequence 类型返回给调用者。

extension MAnySequence {
 static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element {
 return MAnySequenceImpl<Seq>(seq)
 }
}

在实际开发中,我们可能会做一些额外的操作来让 MAnySequence 提供一个初始化方法。

我们来试试 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
 for elt in seq {
 print(elt)
 }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence.make(array))
printInts(MAnySequence.make(array[1 ..< 4]))

完美!

基于函数的类型擦除

有时我们希望对外暴露支持多种类型的方法,但又不想指定具体的类型。一个简单的办法就是,存储那些签名仅涉及到我们想公开的类型的函数,函数主体在底层已知具体实现类型的上下文中创建。

我们一起看看如何运用这种方法来设计 MAnySequence,与前面的实现很类似。它是一个结构体而非类,这是因为它仅仅作为容器使用,不需要有任何的继承关系。

struct MAnySequence<Element>: Sequence {

跟之前一样,MAnySequence 也需要一个可返回的迭代器(Iterator)。迭代器同样被设计为结构体,并持有一个参数为空并返回 Element? 的存储型属性,实际上这个属性是一个函数,被用于 IteratorProtocol 协议的 next 方法中。接下来 Iterator 遵循 IteratorProtocol 协议,并在 next 方法中调用函数:

struct Iterator: IteratorProtocol {
 let _next: () -> Element?

 func next() -> Element? {
  return _next()
 }
}

MAnySequence 与 Iterator 很相似:持有一个参数为空返回 Iterator 类型的存储型属性。遵循 Sequence 协议并在 makeIterator 方法中调用这个属性。

let _makeIterator: () -> Iterator

func makeIterator() -> Iterator {
 return _makeIterator()
}

MAnySequence 的构造函数正是魔法起作用的地方,它接收任意序列作为参数:

init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {

接下来需要在构造函数中包装此序列的功能:

_makeIterator = {

如何生成迭代器?请求 Seq 序列生成:

var iterator = seq.makeIterator()

接下来我们利用自定义的迭代结构体包装序列生成的迭代器,包装后的 _next 属性将会在迭代器协议的 next() 方法中被调用:

   return Iterator(_next: { iterator.next() })
  }
 }
}

接下来展示如何使用 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
 for elt in seq {
  print(elt)
 }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence(array))
printInts(MAnySequence(array[1 ..< 4]))

正确运行,太棒了!

当需要将小部分功能包装为更大类型的一部分时,这种基于函数的类型擦除方法特别实用,这样做就不需要有单独的类来实现被擦除类型的这部分功能了。

比方说你现在想要编写一些适用于各种集合类型的代码,但它真正需要能够对这些集合执行的操作是获取计数并执行从零开始的整数下标。如访问 tableView 数据源。它可能看起来像这样:

class GenericDataSource<Element> {
 let count: () -> Int
 let getElement: (Int) -> Element

 init<C: Collection>(_ c: C) where C.Element == Element,C.Index == Int {
  count = { c.count }
  getElement = { c[$0 - c.startIndex] }
 }
}

GenericDataSource 其他代码可通过调用 count() 或 getElement() 来操作传入的集合。且不会让集合类型破坏 GenericDataSource 泛型参数。

结束语

类型擦除是一种非常有用的技术,它可用来阻止泛型对代码的侵入,也可用来保证接口简单明了。通过将底层类型包装起来,将API与具体的功能进行拆分。这可以通过使用抽象的公共超类和私有子类或将 API 包装在函数中来实现。对于只需要一些功能的简单情况,基于函数类型擦除极其有效。

Swift 标准库提供了几种可直接利用的类型擦除类型。如 AnySequence 包装一个 Sequence,正如其名,AnySequence 允许你对序列迭代而无需知道序列具体的类型。AnyIterator 也是类型擦除的类型,它提供一个类型擦除的迭代器。AnyHashable 也同样是类型擦除的类型,它提供了对Hashable类型访问功能。Swift 还有很多基于集合的擦除类型,你可以通过搜索 Any 来查阅。标准库中也为 Codable API 设计了类型擦除类型: KeyedEncodingContainer 和 KeyedDecodingContainer。它们都是容器协议类型包装器,可用来在不知道底层具体类型信息的情况下实现 Encode 和 Decode。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

作者:Mike Ash,原文链接,原文日期:2017-12-18

译者:rsenjoyer;校对:Yousanflics,numbbbbb;定稿:Forelax

相关文章

  • Swift与C语言指针结合使用实例

    Swift与C语言指针结合使用实例

    这篇文章主要介绍了Swift与C语言指针结合使用实例,本文讲解了用以输入/输出的参数指针、作为数组使用的参数指针、用作字符串参数的指针、指针参数转换的安全性等内容,需要的朋友可以参考下
    2015-05-05
  • 详解Swift中的函数及函数闭包使用

    详解Swift中的函数及函数闭包使用

    Swift的函数在创建和调用时非常简洁,在编写具有闭包特性的函数时同样也相当方便,以下我们就来详解Swift中的函数及函数闭包使用:
    2016-06-06
  • Swift中的可选项Optional解包方式实现原理

    Swift中的可选项Optional解包方式实现原理

    这篇文章主要为大家介绍了Swift中的可选项Optional解包方式实现原理示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Swift读取App的版本信息与PCH文件详解

    Swift读取App的版本信息与PCH文件详解

    这篇文章主要介绍了Swift读取App的版本信息与PCH文件的相关资料,文中通过图文介绍的非常详细,对大家具有一定的参考价值,需要的朋友们下面来一起看看吧。
    2017-03-03
  • Swift代码自定义UIView实现示例

    Swift代码自定义UIView实现示例

    这篇文章主要为大家介绍了Swift如何自定义UIView的实现示例代码,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步,早日升职加薪
    2021-10-10
  • 深入探究Swift枚举关联值的内存

    深入探究Swift枚举关联值的内存

    这篇文章主要给大家介绍了关于Swift枚举关联值的内存的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者使用Swift具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-08-08
  • 举例讲解Swift编程中switch...case语句的用法

    举例讲解Swift编程中switch...case语句的用法

    这篇文章主要介绍了Swift编程中switch...case语句的用法,其中fallthrough关键字在switch语句中的使用是重点,需要的朋友可以参考下
    2016-04-04
  • EvenLoop模型在iOS的RunLoop应用示例

    EvenLoop模型在iOS的RunLoop应用示例

    这篇文章主要为大家介绍了EvenLoop模型在iOS的RunLoop应用示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • swift中自定义正则表达式运算符=~详解

    swift中自定义正则表达式运算符=~详解

    这篇文章主要给大家介绍了关于swift中自定义正则表达式运算符=~的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-12-12
  • Swift中动态调用实例方法介绍

    Swift中动态调用实例方法介绍

    这篇文章主要介绍了Swift中动态调用实例方法介绍,在Swift中有一类很有意思的写法,可以让我们不直接使用实例来调用这个实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法,需要的朋友可以参考下
    2015-01-01

最新评论