使用Swift的类型系统从代码中消除不可能的状态

在Swift中,咱们可使用类,结构和枚举,以及选项和结果,全部这些类型对咱们编写的代码有不一样的含义。编程

咱们在编写代码时有很大的自由,但咱们应该考虑选择类型并利用类型系统,以便咱们的代码可以精确地模拟咱们正在使用的域的数据。swift

放置文本的库可能会定义各类对齐文本的方法,它可使用整数来表示,其中0表示左对齐,1表示右对齐,2表示居中。可是使用整数来表示文本对齐容许没有任何意义的值 - 例如,库应该如何处理值27?使用枚举定义可能的值是有意义的,从而确保只存在有效值。api

URLSession完成处理程序

)在Foundation中,咱们找到一个API,其中使用的类型可能会引发一些混乱。当咱们URLSession从URL加载数据时,咱们必须提供一个带有三个参数的完成处理程序:数组

func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask`
复制代码

这三个参数都是可选的,所以任何或全部参数均可以存在或不存在。这意味着理论上,完成处理程序必须处理八种可能的状态。若是咱们以不一样的方式写出参数,咱们能够更清楚地看到:bash

struct CallbackInfo {
    var data: Data?
    var response: URLResponse?
    var error: Error?
}
复制代码

实际上,并不是全部八种可能状态均可能发生。咱们能够阅读文档以得到关于如何调用回调的提示,可是文档并无像编译器和类型系统那样给咱们提供保证。session

咱们能够考虑哪些国家本身有意义。咱们能够假设,若是咱们得到数据,那么咱们就不会收到错误。反过来讲:若是咱们收到错误,就没有数据。咱们能够将这两种状况建模为枚举:app

enum CallbackInfo2 {
    case success(Data, URLResponse)
    case failure(Error)
}
复制代码

但咱们不肯定这个枚举涵盖了全部可能的状态。在结构的八种可能状态中CallbackInfo,可能会出现一些但CallbackInfo2枚举没法表达的状态。编程语言

经过阅读数据任务方法的文档,很难分辨哪些状况可能发生,哪些状况永远不会发生。能够说,基于枚举的方法CallbackInfo2能够更好地向用户说明须要处理哪些状况。函数

另外一方面,咱们不能说枚举老是比可选参数更好。若是咱们处理四个或五个选项而且可能发生全部可能的组合,那么咱们必须定义一个包含16或32个案例的巨大枚举。这样作可能不会使这种API的使用变得更简单。布局

咱们已经能够用本身的例子说明这个问题。假设失败案例还附带一个可选项Data?

enum CallbackInfo2 {
    case success(Data, URLResponse)
    case failure(Data?, Error)
}
复制代码

鉴于这两种状况均可以包含数据,所以将数据做为可选属性提供而不是隐藏在枚举的关联值中会更有意义,由于这会使访问变得更加困难。

枚举并不老是优于一组选项,反之亦然,但它取决于咱们试图建模的可能状态。

用户会话

第二个例子来自Apple关于Swift的书,Swift编程语言

struct Session {
    var user: User?
    var expired: Bool
}
复制代码

这里咱们有一个用户会话。该user属性是可选的,由于可能没有注册用户。用户会话的这个模型容许四种可能的状态:用户能够存在与否,而且expired 布尔属性能够是truefalse

而后Swift书继续说咱们能够选择将会话建模为枚举,从而消除没有发生的状态,咱们没有用户和过时的会话:

enum Session1 {
    case loggedIn(User)
    case expired(User)
    case notRegistered
}
复制代码

枚举版本比结构更精确地模拟域,由于它只能表示用户会话的可能状态。

可是和前面的例子同样,咱们如今有两个共享相关值的状况,咱们必须切换枚举以便User从会话中提取a 。第三种方法是对会话进行建模,这样能够更轻松地访问用户,而不会变得不那么精确:

struct Session {
    var user: User
    var expired: Bool
}

var session: Session?
复制代码

这里咱们再次使用一个结构,但此次user属性不是可选的。相反,会话自己存储在可选变量中。一种可能的状态是sessionnil,这意味着相同 notRegistered的状况下Session1枚举。在其余两个状态中,存在会话,所以也是用户,而且会话已过时或未过时。

咱们遇到了不少这样的状况:当枚举的多个case共享相同的关联值时,咱们一般能够将enum包装在struct中,并将关联的值拉出到struct的属性中。

将文件名映射到数据

让咱们看另外一个例子。假设咱们有一个文件名数组做为字符串,咱们正在编写一个映射数组并从文件返回数据的函数。该函数的结果类型应该是什么?

该函数能够简单地返回一个数组Data

func readFiles(_ fileNames: [String]) -> [Data] {
    // ... }
复制代码

这样可行,但若是其中一个文件不存在或者没法读取会发生什么?该函数能够省去该文件的数据并返回其他的数据,但做为用户,咱们没法知道哪些文件失败。

结果类型也能够是一个选项数组:

func readFiles(_ fileNames: [String]) -> [Data?] {
    // ... }
复制代码

这样咱们就能够尝试找出哪些文件成功加载了,但咱们不能彻底肯定,由于咱们没法保证结果数组的排序方式与输入数组相同。

咱们可能想要报告有关丢失文件的错误,所以函数可能应该返回文件名以及可选数据值,并结合在元组中:

func readFiles(_ fileNames: [String]) -> [(String, Data?)] {
    // ... }
复制代码

另外一个选择是使整个数组可选。这使得结果所有或所有:咱们要么从全部请求的文件中获取数据,要么使用其中一个文件失败,咱们根本得不到任何结果:

func readFiles(_ fileNames: [String]) -> [(String, Data)]? {
    // ... }
复制代码

即便是像上面这样的简单功能,咱们也能够轻松地想到七种变化。例如,咱们能够决定返回一个Result而不是一个可选项,或者咱们想要包含一个描述不一样类型失败的自定义枚举。在类型之间进行选择彻底取决于对应用程序最有意义的内容。

精确性与易用性

咱们能够更进一步,并尝试强制readFiles函数的输入和输出数组应具备相同的长度。有一些编程语言可让你表达这一点,但在Swift中咱们也有一些能够提供帮助的技巧。

咱们能够尝试以某种方式标记一个长度的数组。而后咱们能够定义一个保留长度并使用此映射来实现的map函数readFiles。可是咱们会推进咱们能够将多少信息投入到类型中,咱们应该问本身增长的复杂性是否值得。

类型不太严格意味着咱们须要更多地信任一段代码的实现。咱们老是能够编写测试来检查代码在咱们提供大量样本输入时的行为方式。

标准库有不少例子,为了简化使用,使用的类型并非描述它们所作的最精确的类型。首先,a Array由整数(Int)索引,而不是由无符号整数(UInt)索引,即便索引从不为负数。

对于count数组或字符串也是如此。金额永远不能是负数,所以使用UInt而不是更精确Int。可是这会使count属性更难以使用,由于在大多数状况下,咱们必须将类型转换为将其Int传递给其余API。

选择正确的类型意味着要在精确性和易用性之间进行权衡。最好的方法是探索不一样的类型,并肯定一种最准确,最好的类型描述咱们描述的任何类型,但不包含任何可能表明不可能状态的垃圾值。

使用幻影类型

在咱们关于 使用Brandon Kase的幻像类型的情节中,咱们讨论了标记类型的概念,以使它们更具描述性,而且更具限制性,意图使用类型系统来帮助防止错误地使用咱们的API。

咱们能够找到在自动布局中使用的幻像类型的实际示例。在那里,视图的 锚点用幻像类型标记以区分水平和垂直锚点。这使得例如将前导锚固定到顶部锚点是不可能的,这是没有意义的。

准确建模特定域数据的问题自动出如今强类型语言的社区中。其中一个主要的榆树开发商理查德·费尔德曼,举行一提及作不可能的状态是不可能的。关于F#,OCaml和Haskell也有相似的讨论,全部讨论如何找到数据表示,只容许你定义有意义的函数。

原文地址:talk.objc.io/episodes/S0…
相关文章
相关标签/搜索