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

(0)

相关推荐

  • 详解Swift中enum枚举类型的用法

    一.引言 在Objective-C语言中,没有实际上是整型数据,Swift中的枚举则更加灵活,开发者可以不为其分配值类型把枚举作为独立的类型来使用,也可以为其分配值,可以是字符,字符串,整型或者浮点型数据. 二.枚举语法 Swift中enum关键字来进行枚举的创建,使用case来创建每一个枚举值,示例如下: //创建姓氏枚举,和Objective-C不同,Swift枚举不会默认分配值 enum Surname { case 张 case 王 case 李 case 赵 } //创建一个枚举类型的

  • Swift内置的数字类型及基本的转换方法

    虽然编写任何编程语言,需要使用不同的变量来存储各种信息.变量不过是保留的内存位置来存储值.这意味着,当创建一个变量,在内存中会保留一些空间. 可能喜欢像存储字符串,字符,宽字符,整数,浮点数,布尔等各种数据类型的信息.根据一个变量的数据类型,操作系统分配内存,并决定什么可以存储保留在存储器. 内置数据类型 Swift 为程序员提供内置以及用户定义的种类数据类型. 以下是声明变量使用最频繁的基本数据类型的列表: Int 或 UInt - 这是用于整数.更具体地可以使用Int32,Int64来定义3

  • Swift教程之基础数据类型详解

    基础类型 虽然Swift是一个为开发iOS和OS X app设计的全新编程语言,但是Swift的很多特性还是跟C和Objective-C相似. Swift也提供了与C和Objective-C类似的基础数据类型,包括整形Int.浮点数Double和Float.布尔类型Bool以及字符串类型String.Swift还提供了两种更强大的基本集合数据类型,Array和Dictionary,更详细的内容可以参考:Collection Types. 跟C语言一样,Swift使用特定的名称来定义和使用变量.同

  • Swift里的值类型与引用类型区别和使用

    Swift里面的类型分为两种: ●值类型(Value Types):每个实例都保留了一分独有的数据拷贝,一般以结构体 (struct).枚举(enum) 或者元组(tuple)的形式出现. ●引用类型(Reference Type):每个实例共享同一份数据来源,一般以类(class)的形式出现. 在这篇博文里面,我们会介绍两种类型各自的优点,以及应该怎么选择使用. 值类型与引用类型的区别 值类型和引用类型最基本的分别在复制之后的结果.当一个值类型被复制的时候,相当于创造了一个完全独立的实例,这个

  • Swift类型创建之自定义一个类型详解

    小伙伴们,Swift中的Bool类型有着非常重要的语法功能,并支撑起了整个Swift体系中的逻辑判断体系,经过老码的研究和学习, Bool类型本身其实是对基础Boolean类型封装,小伙伴们可能咬着手指头问老码,怎么一会Bool类型,一会Boolean类型,其区别在于,前者是基于枚举的组合类型,而后者则是基本类型,只有两种true和false. ####自定义原型 接下老码根据Bool的思想来创建一个OCBool类型,来让小伙伴们了解一下Swift中到底是怎么玩儿的. 来我们先看一下OCBool

  • Swift编程中的一些类型转换方法详解

    验证一个实例的类型'类型转换'在 Swift 语言编程中.它是用来检查实例类型是否属于特定超类或子类或其自己的层次结构定义. Swift 类型转换提供两个操作符:"is" 检查值的类型和 'as' 将类型值转换为不同的类型值. 类型转换还检查实例类型是否符合特定的协议一致性标准. 定义一个类层次结构 类型转换用于检查实例的类型或者它属于特定类型.此外,检查类和它的子类层次结构来检查并转换这些实例,使之作为一个相同的层次结构. 复制代码 代码如下: class Subjects {   

  • Swift教程之枚举类型详解

    枚举定义了一个常用的具有相关性的一组数据,并在你的代码中以一个安全的方式使用它们. 如果你熟悉C语言,你就会知道,C语言中的枚举指定相关名称为一组整数值.在Swift中枚举更为灵活,不必为枚举的每个成员提供一个值.如果一个值(被称为"原始"的值)被提供给每个枚举成员,则该值可以是一个字符串,一个字符,或者任何整数或浮点类型的值. 另外,枚举成员可以指定任何类型,每个成员都可以存储的不同的相关值,就像其他语言中使用集合或变体.你还可以定义一组通用的相关成员为一个枚举,每一种都有不同的一组

  • Swift使用Cocoa中的数据类型教程

    作为对 Objective-C 互用性(互操作性)的一部分,Swift提供快捷高效的方式来处理Cocoa数据类型. Swift 会自动将一些 Objective-C 类型转换为 Swift 类型,以及将 Swift 类型转换为 Objective-C 类型.在 Objective-C 和 Swift 中也有一些具有互用性的数据类型.那些可转换的数据类型或者具有互用性的数据类型被称为bridged数据类型.举个例子,在 Swift 中,您可以将一个Array值传递给一个要求为NSArray对象的方

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

    前言 在 Swift 的世界中,如果我们将协议称之为国王,那么泛型则可以视作皇后,所谓一山不容二虎,当我们把这两者结合起来使用的时候,似乎会遇到极大的困难.那么是否有一种方法,能够将这两个概念结合在一起,以便让它们成为我们前进道路上的垫脚石,而不是碍手碍脚的呢?答案是有的,这里我们将会使用到类型擦除 (Type Erasure) 这个强大的特性. 你也许曾听过类型擦除,甚至也使用过标准库提供的类型擦除类型如 AnySequence.但到底什么是类型擦除? 如何自定义类型擦除? 在这篇文章中,我将

  • Java的类型擦除式泛型详解

    Java选择的泛型类型叫做类型擦除式泛型.什么是类型擦除式泛型呢?就是Java语言中的泛型只存在于程序源码之中,在编译后的字节码文件里,则全部泛型都会被替换为原来的原始类型(Raw Type),并且会在相应的地方插入强制转型的代码. 因此,对于运行期间的Java程序来说ArrayList< Integer>和ArrayList< String>其实是同一个类型.这也就是Java选择的泛型类型叫做类型擦除式泛型的原因. ArrayList<String> stringAr

  • Python对象类型及其运算方法(详解)

    基本要点: 程序中储存的所有数据都是对象(可变对象:值可以修改 不可变对象:值不可修改) 每个对象都有一个身份.一个类型.一个值 例: >>> a1 = 'abc' >>> type(a1) str 创建一个字符串对象,其身份是指向它在内存中所处的指针(在内存中的位置) a1就是引用这个具体位置的名称 使用type()函数查看其类型 其值就是'abc' 自定义类型使用class 对象的类型用于描述对象的内部表示及其支持的方法和操作 创建特定类型的对象,也将该对象称为该类

  • 深度思考JDK8中日期类型该如何使用详解

    在JDK8之前,处理日期时间,我们主要使用3个类, Date . SimpleDateFormat 和 Calendar . 这3个类在使用时都或多或少的存在一些问题,比如 SimpleDateFormat 不是线程安全的, 比如 Date 和 Calendar 获取到的月份是0到11,而不是现实生活中的1到12,关于这一点,<阿里巴巴Java开发手册>中也有提及,因为很容易犯错: 不过,JDK8推出了全新的日期时间处理类解决了这些问题,比如 Instant . LocalDate . Loc

  • swift where与匹配模式的实例详解

    swift where与匹配模式的实例详解 前言: 在众多 Swift 提供给 Objective-C 程序员使用的新特性中,有个特性把自己伪装成一个无聊的老头,但是却在如何优雅的解决"鞭尸金字塔"的问题上有着巨大的潜力.很显然我所说的这个特性就是 switch 语句, 对于很多 Objective-C 程序员来说,除了用在 Duff's Device 上比较有趣之外,switch 语句非常笨拙,与多个 if 语句相比,它几乎没有任何优势. 1.基本使用 Swift中switch语句c

  • swift MD5加密源码的实例详解

    swift MD5加密源码的实例详解 因为MD5加密是不可逆的,所以一般只有MD5加密的算法,而没有MD5解密的算法. 创建一个Sting+MD5.Swift字符串分类文件(同时此处需要创建一个bridge.h桥接文件,引入这个头文件 #import <CommonCrypto/CommonDigest.h>,md5加密方法需要使用的文件) 1.bridge.h桥接文件如下: #ifndef bridge_h #define bridge_h #import <CommonCrypto/

  • C/C++ ip地址与int类型的转换实例详解

    C/C++ ip地址与int类型的转换实例详解 前言 最近看道一个面试题目,大体意思就是将ip地址,例如"192.168.1.116"转换成int类型,同时还能在转换回去 思路 ip地址转int类型,例如ip为"192.168.1.116",相当于"."将ip地址分为了4部分,各部分对应的权值为256^3, 256^2, 256, 1,相成即可 int类型转ip地址,思路类似,除以权值即可,但是有部分字符串的操作 实现代码 #include &l

  • Java的静态类型检查示例代码详解

    关于静态类型检查和动态类型检查的解释: 静态类型检查:基于程序的源代码来验证类型安全的过程: 动态类型检查:在程序运行期间验证类型安全的过程: Java使用静态类型检查在编译期间分析程序,确保没有类型错误.基本的思想是不要让类型错误在运行期间发生. 在各色各样的编程语言中,总共存在着两个类型检查机制:静态类型检查和动态类型检查. 静态类型检查是指通过对应用程序的源码进行分析,在编译期间就保证程序的类型安全. 动态类型检查是在程序的运行过程中,验证程序的类型安全.在Java中,编译期间使用静态类型

  • 对python xlrd读取datetime类型数据的方法详解

    使用xlrd读取出来的时间字段是类似41410.5083333的浮点数,在使用时需要转换成对应的datetime类型,下面代码是转换的方法: 首先需要引入xldate_as_tuple函数 from xlrd import xldate_as_tuple 使用方法如下: #d是从excel中读取出来的浮点数 xldate_as_tuple(d,0) xldate_as_tuple第二个参数有两种取值,0或者1,0是以1900-01-01为基准的日期,而1是1904-01-01为基准的日期.该函数

  • java 使用idea将工程打成jar并创建成exe文件类型执行的方法详解

    第一部分: 使用idea 打包工程jar 1.准备好一份 开发好的 可执行的 含有main方法的 工程. 例如:我随便写的main方法 public static void main(String[] args) throws IOException { Properties properties = System.getProperties(); String osName = properties.getProperty("os.name"); System.out.println

随机推荐