前言混淆数据可以说是一般应用程序中最常见的错误和问题来源之一。虽然Swift通过其强大的类型系统和复杂的编译器帮助我们避免了许多歧义来源——只要我们不能在编译时保证某个数据总是满足我们的要求,我们就总是有风险模糊或不可预测的状态。本周,让我们来看看一种技术,它允许我们利用Swift的类型系统在编译时执行更多类型的数据验证——通过使用幻像类型消除更多潜在的歧义来源并帮助我们维护类型安全(幻影类型)。定义明确,但仍含糊不清例如,假设我们正在开发一个文本编辑器,尽管它最初只支持纯文本文件-随着时间的推移,我们还添加了对编辑HTML文档以及PDF预览的支持。为了能够尽可能多地重用我们的原始文档处理代码,我们继续使用我们开始使用的相同文档模型-只是现在它获得了一个格式属性,告诉我们我们正在处理的文档类型:structDocument{enumFormat{casetextcasehtmlcasepdf}varformat:Formatvardata:DatavarmodificationDate:Datevarauthor:Author}能够避免代码重复当然很好,枚举是我们在处理不同的时候模型的格式或变体通常是对事物建模的好方法,但这种设置实际上最终会产生相当多的歧义。例如,我们可能拥有仅在调用给定格式的文档时才有意义的API——例如这个打开文本编辑器的函数,它假定传递给它的任何文档都是文本文档:funcopenTextEditor(fordocument:Document){lettext=String(decoding:document.data,as:UTF8.self)leteditor=TextEditor(text:text)...}虽然如果我们不小心将HTML文档传递给上面的功能(毕竟HTML只是文本),但是尝试以这种方式打开PDF可能会呈现出完全无法理解的内容,我们的文本编辑功能将无法使用,我们的应用程序甚至可能最终崩溃。在为任何其他特定格式编写代码时,我们不断遇到同样的问题,例如,如果我们想通过实现解析器和专用编辑器来改善编辑HTML文档的用户体验:funcopenHTMLEditor(fordocument:Document){//就像我们上面的文本编辑函数一样,//这个函数假设它总是传递一个HTML文档。letparser=HTMLParser()lethtml=parser.parse(document.data)leteditor=HTMLEditor(html:html)...}关于如何解决上述问题的初步想法可能是编写一个包装函数,切换到文档的传递格式,然后为每个案例打开正确的编辑器。然而,虽然这适用于文本和HTML文档,但由于PDF文档在我们的应用程序中不可编辑-我们将被迫抛出错误、触发断言或以其他方式失败:funcopenEditor(fordocument:Document){switchdocument.format{case.text:openTextEditor(for:document)case.html:openHTMLEditor(for:document)case.pdf:assertionFailure("CannoteditPDFdocuments")}}以上不是很好,因为它需要我们作为开发人员要跟踪我们在任何给定代码路径上处理的文件类型,我们可能犯的任何错误只能在运行时被捕获——编译器根本没有足够的信息在编译时进行这样的检查。因此,虽然我们的“文档”模型乍一看似乎非常优雅和完整,但事实证明它并不是当前情况的正确解决方案。看来我们需要一个协议!解决上述问题的一种方法是使Document成为协议而不是具体类型,并将其所有属性(格式除外)作为要求:protocolDocument{vardata:Data{get}varmodificationDate:Date{get}varauthor:Author{get}}通过以上更改,我们现在可以为三种文档格式中的每一种实现专用类型,并使这些类型符合我们新的文档协议——例如:structTextDocument:Document{vardata:DatavarmodificationDate:Datevarauthor:Author}上述方法的好处在于,它允许我们实现可以对任何Document进行操作的通用函数,并且该实现只接受某个具体类型的特定API://这个函数可以保存任何文件,//因此它接受任何符合我们新文档协议的内容。funcsave(_document:Document){...}//我们现在只能将文本文件传递给我们的函数,//它会打开一个文本编辑器。funcopenTextEditor(fordocument:TextDocument){...}我们在上面所做的基本上是将过去的运行时检查变成编译时验证——因为编译器现在能够检查我们是否总是每个我们的API以正确的格式交付文件,这是一个很大的改进。然而,通过实施上述更改,我们也失去了我们原来实施的优势——代码重用。由于我们现在使用一种协议来表示所有文档格式,因此我们需要为三种文档类型中的每一种编写完全重复的模型实现,并为我们将来可能添加的任何其他格式提供支持。引入Phantom类型如果我们能找到一种方法为所有格式重用相同的文档模型,同时在编译时验证我们的格式特定代码,那不是很好吗?事实证明,我们之前的代码行确实给了我们一个提示来实现这一点:类型本身传递我们希望字符串解码的编码-在本例中为UTF8。这真的很有趣。如果我们深入挖掘,我们会发现Swift标准库将我们上面提到的UTF8类型定义为另一个名为Unicode的类似命名空间的枚举中的无大小写枚举。enumUnicode{enumUTF8{}...}typealiasUTF8=Unicode.UTF8请注意,如果您查看UTF8类型的实际实现,它确实包含一个仅用于向后兼容Swift3的私有案例。我们的内容在这里看到的是一种称为幻像类型的技术——当类型被用作标记,而不是被实例化来表示值或对象时。事实上,由于以上枚举都没有任何暴露的案例,所以它们甚至无法被实例化!让我们看看是否可以使用相同的技术来解决我们的文档困境。我们首先将Document简化为一个结构,只是这一次我们将删除它的格式属性(和关联的枚举)并使其成为覆盖任何格式类型的泛型-像这样:structDocument
