Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Swift 中的幻象类型

Swift 中的幻象类型

作者头像
Swift社区
发布于 2022-05-19 00:23:51
发布于 2022-05-19 00:23:51
1.5K00
代码可运行
举报
文章被收录于专栏:Swift社区Swift社区
运行总次数:0
代码可运行

前言

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

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Document {
    enum Format {
        case text
        case html
        case pdf
    }

    var format: Format
    var data: Data
    var modificationDate: Date
    var author: Author
}

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func openTextEditor(for document: Document) {
    let text = String(decoding: document.data, as: UTF8.self)
    let editor = TextEditor(text: text)
    ...
}

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func openHTMLEditor(for document: Document) {
    // 就像我们上面用于文本编辑的函数一样,
    // 这个函数假设它总是被传递给HTML文档。
    let parser = HTMLParser()
    let html = parser.parse(document.data)
    let editor = HTMLEditor(html: html)
    ...
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)都作为要求:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protocol Document {
    var data: Data { get }
    var modificationDate: Date { get }
    var author: Author { get }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct TextDocument: Document {
    var data: Data
    var modificationDate: Date
    var author: Author
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这个函数可以保存任何文件,
// 所以它接受任何符合我们的新文档协议。
func save(_ document: Document) {
    ...
}

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

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

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

引入幻象类型

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let text = String(decoding: document.data, as: UTF8.self)

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum Unicode {
    enum UTF8 {}
    ...
}

typealias UTF8 = Unicode.UTF8

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Document<Format> {
    var data: Data
    var modificationDate: Date
    var author: Author
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum DocumentFormat {
    enum Text {}
    enum HTML {}
    enum PDF {}
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func openTextEditor(for document: Document<DocumentFormat.Text>) {
    ...
}

func openHTMLEditor(for document: Document<DocumentFormat.HTML>) {
    ...
}

func openPreview(for document: Document<DocumentFormat.PDF>) {
    ...
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func save<F>(_ document: Document<F>) {
    ...
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typealias TextDocument = Document<DocumentFormat.Text>
typealias HTMLDocument = Document<DocumentFormat.HTML>
typealias PDFDocument = Document<DocumentFormat.PDF>

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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使用幻象类型来确保在传递各种测量值时的类型安全——例如度数、长度和重量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let meters = Measurement<UnitLength>(value: 5, unit: .meters)
let degrees = Measurement<UnitAngle>(value: 90, unit: .degrees)

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

结论

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

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

- EOF -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Swift社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Swift 中的 Phantom(幻象)类型
模糊的数据可以说是一般应用程序中最常见的错误和问题的来源之一。虽然 Swift 通过其强大的类型系统和完善的编译器帮助我们避免了许多含糊不清的来源——但只要我们无法在编译时保证某个数据总是符合我们的要求,就总是有风险,我们最终会处于含糊不清或不可预测的状态。
韦弦zhy
2022/04/26
9090
Swift代码中的嵌套命名法
Swift支持与其他类型嵌套命名,尽管它还没有专用的命名关键词。下面我们来看看,如何使用类型嵌套来优化我们代码的结构。
莫空9081
2021/03/01
1.7K0
Swift基础语法(四)
在Swift5之前,我们一般是采用上面的方式来处理异常,在Swift5之后,苹果推出了一个Result枚举,Result枚举可以更加优雅地去处理异常。
拉维
2020/07/06
4K0
Swift基础语法(四)
开心档之Swift 访问控制
你可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、函数、初始化方法、基本类型、下标索引等设置访问级别。
爱学iOS的小麦子
2023/03/06
1K0
【面试必备】Swift 面试题及其答案
答案:optional类型被用来表示任何类型的变量都可以表示缺少值。在Objective-C中,引用类型的变量是可以缺少值得,并且使用nil作为缺少值。基本的数据类型如int 或者float没有这种功能。
编程怪才-凌雨画
2021/01/26
2.9K0
【面试必备】Swift 面试题及其答案
Swift 中的 Sendable 和 @Sendable 闭包
Sendable 和 @Sendable 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。
韦弦zhy
2022/11/14
1.5K0
Swift 中的 Sendable 和 @Sendable 闭包
Swift 5.6 新特性
Swift 5.6 之前只有#available表示可用,Swift 5.6 之后增加了#unavailable表示不可用,二者意思相反。
YungFan
2022/03/30
1.2K0
对Swift中some和any关键字的理解
在最新Swift版本中(Xcode14,Swift5.7),如果协议中有使用泛型,则如果要将此协议作为参数类型,必须使用any关键字进行修饰。其实在Swift5.1中也引入过一个some关键字,any和some都适用于协议,这两个关键字从语义上和写法上对泛型的使用进行了优化。
珲少
2022/11/14
1.1K1
46 道 Swift 常见面试题解
3、Set 独有的方法有哪些? 4、实现一个 min 函数,返回两个元素较小的元素 5、map、filter、reduce 的作用 6、map 与 flatmap 的区别 7、什么是 copy on write 8、如何获取当前代码的函数名和行号 9、如何声明一个只能被类 conform 的 protocol 10、guard 使用场景 11、defer 使用场景 12、String 与 NSString 的关系与区别 13、怎么获取一个 String 的长度 14、如何截取 String 的某段字符串 15、throws 和 rethrows 的用法与作用 16、try?和 try!是什么意思 17、associatedtype 的作用 18、什么时候使用 final 19、public 和 open 的区别 20、声明一个只有一个参数没有返回值闭包的别名
Swift社区
2021/11/26
5.4K0
【面试必备】Swift 面试题及其答案
答案:optional 类型被用来表示任何类型的变量都可以表示缺少值。在 Objective-C 中,引用类型的变量是可以缺少值,并且使用 nil 作为缺少值。基本的数据类型如 int 或者 float 没有这种功能。
Swift社区
2021/11/26
7K0
戴铭的 Swift 小册子
越来越多同学打算开始用 Swift 来开发了,可很多人以前都没接触过 Swift。这篇和我以前文章不同的是,本篇只是面向 Swift 零基础的同学,内容主要是一些直接可用的小例子,例子可以直接在工程中用或自己调试着看。
Swift社区
2021/12/06
2.3K0
戴铭的 Swift 小册子
了解 Swift 的 Result 类型
通常希望函数成功返回一些数据,或者如果失败则返回错误。我们通常使用throwing函数对此建模,因为如果函数调用成功,我们将获得数据,但是如果抛出错误,则将运行catch代码块,因此我们可以独立处理这两个函数。但是,如果函数调用没有立即返回怎么办?
韦弦zhy
2020/09/14
2.7K0
了解 Swift 的 Result 类型
Swift 5.5 新特性
Swift 5.5 内置于 Xcode 13,虽然版本号只增加了 0.1,看似是一个小版本升级,但却带来了非常多的新内容,其中最大的更新是引入了全新的并发编程方式。
YungFan
2021/07/16
2.1K0
Swift 5.6到5.10新特性整理
当你编写涉及共享状态的代码时,如果你不确保这个共享状态在跨线程使用时是安全的,你就会在许多地方遇到数据竞争的问题。
小刀c
2024/04/03
2.2K0
Swift 5.6到5.10新特性整理
AttributedString——不仅仅让文字更漂亮
在WWDC 2021上,苹果为开发者带来了有一个期待已久的功能——AttributedString,这意味着Swift开发人员不再需要使用基于Objective-C的NSAttributedString来创建样式化文本。本文将对其做全面的介绍并演示如何创建自定义属性。
东坡肘子
2022/07/28
4K0
AttributedString——不仅仅让文字更漂亮
WWDC 2021新Formatter API:新老比较及如何自定义
在WWDC 2021的What's in Foundation专题中,苹果隆重介绍了适用于Swift的新Formatter API。网上已经有不少文章对新API的用法进行了说明。本文将通过介绍如何创建符合新API的Formatter,让读者从另一个角度了解新Formatter API的设计机制;并对新旧两款API进行比较。
东坡肘子
2022/07/28
1.4K0
WWDC 2021新Formatter API:新老比较及如何自定义
Swift 项目中涉及到 JSONDecoder,网络请求,泛型协议式编程的一些记录和想法
最近项目开发一直在使用 swift,因为 HTN 项目最近会有另外一位同事加入,所以打算对最近涉及到的一些技术和自己的一些想法做个记录,同时也能够方便同事熟悉代码。
用户7451029
2020/06/16
6.8K0
Swift 5.5 新特性
SE-0296提案终于为开发者带来了期待已久的 async/await,语法基本上和javascript中的很像。
小刀c
2022/08/16
2.7K0
Swift 5.5 新特性
Swift 周报 第三十四期
本期是 Swift 编辑组自主整理周报的第三十四期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。
Swift社区
2023/09/06
2600
Swift 周报 第三十四期
Codable 解析 JSON 配置默认值
2017年推出的 Codable 无疑是 Swift 的一大飞跃。尽管当时社区已经构建了多种用于本地 Swift 值和 JSON 之间 的编解码工具,但由于 Codable 与 Swift 编译器本身的集成,提供了前所未有的便利性,使我们能够通过使可解码类型遵守 Decodable 协议来定义可解码类型,例如:
韦弦zhy
2021/04/07
1.9K0
相关推荐
Swift 中的 Phantom(幻象)类型
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验