我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解Swift 中的幻象类型

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解Swift 中的幻象类型

前言

模糊的数据可以说是一般应用程序中最常见的错误和问题的来源之一。虽然 Swift 通过其强大的类型系统和完善的编译器帮助我们避免了许多含糊不清的来源——但只要我们无法在编译时保证某个数据总是符合我们的要求,就总是有风险,我们最终会处于含糊不清或不可预测的状态。

本周,让我们来看看一种技术,它可以让我们利用 Swift 的类型系统在编译时执行更多种类的数据验证——消除更多潜在的歧义来源,并帮助我们在整个代码库中保持类型安全——通过使用幻象类型(phantom types)。

定义良好,但仍然含糊不清

举个例子,假设我们正在开发一个文本编辑器,虽然它最初只支持纯文本文件——随着时间的推移,我们还增加了对编辑HTML文档的支持,以及PDF预览。

为了能够尽可能多地重复使用我们原来的文档处理代码,我们继续使用与开始时相同的Document模型——只是现在它获得了一个Format属性,告诉我们正在处理什么样的文档:

struct Document {
    enum Format {
        case text
        case html
        case pdf
    }
    var format: Format
    var data: Data
    var modificationDate: Date
    var author: Author
}

能够避免代码重复当然是件好事,而且枚举是当我们在处理一个模型的不同格式或变体时一般情况下建模 的好方法,但是上述那种设置实际上最终会造成相当多的模糊性。

例如,我们可能有一些API,只有在调用给定格式的文档时才有意义——比如这个打开文本编辑器的函数,它假定任何传入它的Document都是文本文档:

func openTextEditor(for document: Document) {
    let text = String(decoding: document.data, as: UTF8.self)
    let editor = TextEditor(text: text)
    ...
}

虽然如果我们不小心将一个HTML文档传递给上述函数并不是世界末日(HTML毕竟只是文本),但试图以这种方式打开一个PDF,很可能会导致呈现出完全无法理解的东西,我们的文本编辑功能将无法工作,我们的应用程序甚至可能最终崩溃。

我们在编写任何其他特定格式的代码时都会不断遇到同样的问题,例如,如果我们想通过实现一个解析器和一个专门的编辑器来改善编辑HTML文档的用户体验:

func openHTMLEditor(for document: Document) {
    // 就像我们上面用于文本编辑的函数一样,
    // 这个函数假设它总是被传递给HTML文档。
    let parser = HTMLParser()
    let html = parser.parse(document.data)
    let editor = HTMLEditor(html: html)
    ...
}

一个关于如何解决上述问题的初步想法可能是编写一个包装函数,切换到所传递文档的格式,然后为每种情况打开正确的编辑器。然而,虽然这对文本和HTML文档很有效,但由于PDF文档在我们的应用程序中是不可编辑的——当遇到PDF时,我们将被迫抛出一个错误,触发一个断言,或以其他方式失败:

func openEditor(for document: Document) {
    switch document.format {
    case .text:
        openTextEditor(for: document)
    case .html:
        openHTMLEditor(for: document)
    case .pdf:
        assertionFailure("Cannot edit PDF documents")
    }
}

上述情况不是很好,因为它要求我们作为开发者始终跟踪我们在任何给定的代码路径中所处理的文件类型,而我们可能犯的任何错误只能在运行时被发现——编译器根本没有足够的信息可以在编译时进行这种检查。

因此,尽管我们的 "Document "模型乍一看可能非常优雅和完善,但事实证明,它并不完全是手头情况的正确解决方案。

看起来我们需要一个协议!

解决上述问题的一个方法是把Document变成一个协议,而不是作为一个具体的类型,把它的所有属性(除了format)都作为要求:

protocol Document {
    var data: Data { get }
    var modificationDate: Date { get }
    var author: Author { get }
}

有了上述变化,我们现在可以为我们的三种文档格式中的每一种实现专门的类型,并让这些类型都符合我们新的文档协议——比如这样:

struct TextDocument: Document {
    var data: Data
    var modificationDate: Date
    var author: Author
}

上述方法的好处是,它使我们既能实现可以对任何Document进行操作的通用功能,又能实现只接受某种具体类型的特定API:

// 这个函数可以保存任何文件,
// 所以它接受任何符合我们的新文档协议。
func save(_ document: Document) {
    ...
}

// 我们现在只能向我们的函数传递文本文件,
// 即打开一个文本编辑器。
func openTextEditor(for document: TextDocument) {
    ...
}

我们在上面所做的基本上是将以前在运行时进行的检查转为在编译时进行验证——因为编译器现在能够检查我们是否总是向我们的每个API传递正确格式的文件,这是一个很大的进步。

然而,通过执行上述改变,我们也失去了我们最初实现的优点——代码重用。由于我们现在使用一个协议来表示所有的文档格式,我们将需要为我们的三种文档类型中的每一种编写完全重复的模型实现,以及为我们将来可能增加的任何其他格式提供支持。

引入幻象类型

如果我们能找到一种方法,既能为所有格式重用相同的Document模型,又能在编译时验证我们特定格式的代码,岂不妙哉?事实证明,我们之前的一行代码实际上可以给我们一个实现这一目标的提示:

let text = String(decoding: document.data, as: UTF8.self)

当把Data转换为String时,就像我们上面做的那样,我们通过传递对该类型本身的引用来传递我们希望字符串被解码的编码——在本例中是UTF8。这真的很有趣。如果我们再深入一点,就会发现 Swift 标准库将我们上面提到的UTF8类型定义为另一个类似命名空间的枚举中的一个无大小写枚举,称为Unicode

enum Unicode {
    enum UTF8 {}
    ...
}
typealias UTF8 = Unicode.UTF8

请注意,如果你看一下UTF8类型的实际实现,它确实包含一个私有case,只是为了向后兼容 Swift 3 而存在。

我们在这里看到的是一种被称为幻象类型的技术——当类型被用作标记,而不是被实例化来表示值或对象时。事实上,由于上述枚举都没有任何公开的情况,它们甚至不能被实例化!

让我们看看是否可以用同样的技术来解决我们的Document困境。我们首先将Document还原成一个结构体,只是这次我们将删除它的format属性(以及相关的枚举),而将它变成一个覆盖任何Format类型的泛型——比如这样:

struct Document<Format> {
    var data: Data
    var modificationDate: Date
    var author: Author
}

受标准库的Unicode枚举及其各种编码的启发,我们将定义一个类似的枚举——DocumentFormat——作为三个无大小写的枚举的命名空间,每种格式都有一个:

enum DocumentFormat {
    enum Text {}
    enum HTML {}
    enum PDF {}
}

请注意,这里不涉及任何协议——任何类型都可以被用作格式,因为就像String和它的各种编码一样,我们将只使用文档的Format类型作为编译时的标记。这将使我们能够像这样写出我们特定格式的API:

func openTextEditor(for document: Document<DocumentFormat.Text>) {
    ...
}
func openHTMLEditor(for document: Document<DocumentFormat.HTML>) {
    ...
}
func openPreview(for document: Document<DocumentFormat.PDF>) {
    ...
}

当然,我们仍然可以编写不需要任何特定格式的通用代码。例如,这里我们可以把之前的saveAPI变成一个完全通用的函数:

func save<F>(_ document: Document<F>) {
    ...
}

然而,总是输入Document<DocumentFormat.Text>来引用一个文本文档是相当乏味的,所以让我们也使用类型别名为每种格式定义速记。这将给我们提供漂亮的、有语义的名字,而不需要任何重复的代码:

typealias TextDocument = Document<DocumentFormat.Text>
typealias HTMLDocument = Document<DocumentFormat.HTML>
typealias PDFDocument = Document<DocumentFormat.PDF>

在涉及到特定格式的扩展时,幻象类型也确实大放异彩,现在可以直接使用 Swift 强大的泛型系统和泛型型约束来实现。例如,我们可以用一个生成NSAttributedString的方法来扩展所有文本文档:

extension Document where Format == DocumentFormat.Text {
    func makeAttributedString(withFont font: UIFont) -> NSAttributedString {
        let string = String(decoding: data, as: UTF8.self)

        return NSAttributedString(string: string, attributes: [
            .font: font
        ])
    }
}

由于我们的幻象类型在最后只是普通的类型——我们也可以让它们遵守协议,并使用这些协议作为泛型约束。例如,我们可以让我们的一些DocumentFormat类型遵守Printable协议,然后我们可以在打印代码中使用这些协议作为约束条件。这里有大量的可能性。

一个标准的模式

起初,幻象类型在 Swift 中可能看起来有点 "格格不入"。然而,虽然 Swift 并没有像更多的纯函数式语言(如Haskell)那样为幻象类型提供一流的支持,但在标准库和苹果平台SDK的许多不同地方都可以找到这种模式。

例如,FoundationMeasurement API使用幻象类型来确保在传递各种测量值时的类型安全——例如度数、长度和重量:

let meters = Measurement<UnitLength>(value: 5, unit: .meters)
let degrees = Measurement<UnitAngle>(value: 90, unit: .degrees)

通过使用幻影类型,上述两个测量值不能被混合,因为每个值是哪种单位,都被编码到该值的类型中。这可以防止我们不小心将一个长度传递给一个接受角度的函数,反之亦然——就像我们之前防止文档格式被混淆一样。

结论

使用幻象类型是一种非常强大的技术,它可以让我们利用类型系统来验证一个特定值的不同变体。虽然使用幻象类型通常会使API更加冗长,而且确实伴随着泛型的复杂性——当处理不同的格式和变体时,它可以让我们减少对运行时检查的依赖,而让编译器来执行这些检查。

就像一般的泛型一样,我认为在部署幻象类型之前,首先要仔细评估当前的情况,这很重要。就像我们最初的Document模型并不是手头任务的正确选择,尽管它的结构很好,但如果部署在错误的情况下,幻象类型会使简单的设置变得更加复杂。像往常一样,它归结为为工作选择正确的工具。

到此这篇关于Swift 中的幻象类型的文章就介绍到这了,更多相关Swift 幻象类型内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解Swift 中的幻象类型

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Swift图表使用Foudation库中测量类型详解

这篇文章主要为大家介绍了Swift图表使用Foudation库中测量类型详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

Java对象类型的判断详解

在Java中,可以使用`instanceof`运算符来判断一个对象的类型。`instanceof`运算符用于检查一个对象是否是一个特定类的实例,或者是其子类的实例。它的使用方式是:```javaobject instanceof Class
2023-08-15

Swift中的类型占位符怎么使用

本篇内容介绍了“Swift中的类型占位符怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Swift 的类型推断能力从一开始就是语言的核
2023-07-02

Javascript Object对象类型使用详解

面向对象编程(Object Oriented Programming)将现实世界中的复杂关系抽象成一个个对象,通过对象之间的分工合作对现实世界进行模拟,这篇文章主要介绍了Javascript Object对象类型使用详解
2022-11-13

详解Android中的Context抽象类

关于Context我们首先应该知道: (1)它描述的是一个应用程序环境的信息,即上下文。 (2)该类是一个抽象(abstract class)类,Android提供了该抽象类的具体实现类(后面我们会讲到是ContextIml类)。 (3)通
2022-06-06

详解Python中的断点类型

Python提供了多种断点类型,每种类型具有不同功能。线断点暂停执行在特定行上;条件断点在满足特定条件时暂停执行;异常断点在发生异常时暂停执行;函数断点在进入或离开指定函数时暂停执行;模块断点在导入或执行指定模块时暂停执行;类断点在执行类时暂停执行。此外,还有高级断点类型,如断点命令、日志断点和条件表达式断点。选择正确的断点类型取决于要调试的特定问题。
详解Python中的断点类型
2024-04-02

python中的类型和对象

type 类继承object类,由type自己实例化而来object由type类实例化而来,object没有基类list类有type类实例化来,继承自object类mylist由list类实例化而来,不继承任何类type(list)查看li
2023-01-31

详解C++11中的类型推断

C++11中为了更好的支持泛型编程,提供了 auto和decltype两个关键词,目的就是提供编译阶段的自动类型推导,这篇文章主要介绍了C++11中的类型推断,需要的朋友可以参考下
2023-01-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录