使非法状态不可表示

栏目: IOS · 发布时间: 6年前

内容简介:原文链接=作者=Ole Begemann原文日期=2018-03-27

原文链接= https://oleb.net/blog/2018/03/making-illegal-states-unrepresentable/

作者=Ole Begemann

原文日期=2018-03-27

我曾表述过 Swift 强类型系统的一个主要优点是能形成自动化和编译器强制执行的文档。

类型是编译强制执行的文档

由于类型为函数的行为建立了一种“界限”,因此一个易用的 API 应该有精心选择的输入输出类型。

仔细思考以下 Swift 函数声明的简单例子:

func / (dividend: Int, divisor: Int) -> Int

在不需要任何函数实现的情况下,你就可以推断出这应该是 整型除法 ,因为返回的类型不可能是小数。相较之下,如果函数的返回类型是既可以表示整型,也可以表示浮点型数值的 NSNumber ,那么你就会信任这种充分文档化的做法。

随着类型系统的表现越来越好,这种使用类型来记录函数行为的技巧,变得越来越有用。如果 Swift 有一个代表“除了 0 之外的整型”,那么除法的函数可能就会变成如下的形式:

func / (dividend: Int, divisor: NonZeroInt) -> Int

由于类型检查不允许传入的除数为 0,因此你就不会疑惑函数如何处理除数为 0 的错误。这是一个陷阱吗?这会返回一个垃圾值吗?这就是为什么函数的第一个变量必须单独文档化。

使非法状态成为不可能

我们可以把这个观点转换为一条通用规则: 使用类型让你的程序无法表现非法的状态

如果你想学习更多相关知识,你可以查看 Brandon Williams 和 Stephen Celis 的最新视频系列 Point-Free 。他们讲了很多这方面的知识和相关的话题,前八集真是的特别棒,我强烈推荐大家去订阅,你将会学到很多。

第四集 关于代数数据类型( algebraic data types )的视频中,Brandon 和 Stephen 讨论了如何组合 enumsstructs (或者 tuples )用以设计能够精确表示所期望状态的类型,并且让所有所有非法的状态不可表征。在视频的最后,他们将 Apple 的 URLSession API 作为反面教材,因为这个 API 没有使用最合适的类型,这就引发了本文子标题的思考。

URLSession

Swift 的类型系统比 Objective-C 的更富有表现力。然而,很多 Apple 自身的 API 也没有利用这个优势,可能是因为缺少资源去更新老旧的 API 或者为了维持 Objective-C 的兼容性。

在 iOS 中发起一个 网络请求 的通用使用案例:

class URLSession {
    func dataTask(with url: URL,
        completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
        -> URLSessionDataTask
}

在完成的回调处理中接收了三个可选值: Data? URLResponse? Error? 。这将产生 2 × 2 × 2 = 8 种,但是其中有多少种是合法的呢?

引述 Brandon 和 Stephen 的观点,这里面有很多可表示的状态都毫无意义。很多明显就毫无意义,并且通过 Apple 代码,我们发现完成处理的值,永远不可能全为 nil 或全为非 nil

响应和错误能够同时非 nil

其他状态就很棘手了,在这里 Brandon 和 Stephen 犯了一点小错误:他们以为 API 要么返回一个有效的 DataURLResponse ,要么返回一个 Error 。毕竟接口不可能同时返回一个非 nil 的响应和错误。看起来很有道理,对不对?

但事实上这是错误的。 URLResponse 封装了服务器的 HTTP 响应头部 ,只要接收到一个有效的响应头部, URLSession API 就会一直给你提供这个值,即使在后续的阶段请求错误了(例如取消和超时)。因而 API 的完成处理中包含一个有效的 URLResponse 和非 nil 的错误值(但是没有 Data )。

如果你对 URLSession 基于代理的 API 比较熟悉的话,就没什么好疑惑的,因为代理方法是分成 didReceiveResponse didReceiveData 。实际上, dataTask​(with:​completionHandler:) 的文档 也提到了这个问题:

如果收到服务器的响应,那么无论请求成功或失败,响应参数都会包含信息。

不过,我敢打赌 Cocoa 开发人员普遍对此抱有误解。仅仅在过去的四周,我就看到 文章 的作者犯了同样的错误(至少没有领悟其中的真谛)。

我非常喜欢这个讽刺的例子:实际上 Brandon 和 Stephen 虽然指出了由于选择的类型很差而导致的 API 缺陷,但他们却犯了一个错误,而如果原始 API 使用了更好的类型,那么这个错误就能够避免,这反而证明了他们精心制作的观点:一个有更加严格类型的 API 能够避免意外的误用。

示例代码

如果你想检验 URLSession 的功能,你可以复制以下代码到 Swift playground:

import Foundation
import PlaygroundSupport

// If this 404s, replace with a URL to any other large file
let bigFile = URL(string: "https://speed.hetzner.de/1GB.bin")!

let task = URLSession.shared.dataTask(with: bigFile) { (data, response, error) in
    print("data:", data as Any)
    print("response:", response as Any)
    print("error:", error as Any)
}
task.resume()

// Cancel download after a few seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    task.cancel()
}
PlaygroundPage.current.needsIndefiniteExecution = true

这段代码首先下载一个大文件,然后在几秒后取消。最后,完成的处理中返回了一个非 nil 的响应和错误。

(这里假设指定的时间间隔内,能够获取到服务器响应的头部,但不能完成下载。如果你在非常慢或者变态好的网络环境下,你应该调整这个时间参数)

正确类型是什么样的?

Brandon 和 Stephen 随后在 Point-Free 的第九集视频 中发布了他们对问题的跟进。他们认为“正确”的完成处理的参数类型应该是:

(URLResponse?, Result<Data, Error>)

我不同意,因为能获取到有效的数据,但没有响应似乎是不可能的。我认为应该是这样的:

Result<(Data, URLResponse), (Error, URLResponse?)>

解读:你将要么得到数据和一个响应(不能为 nil ),要么得到一个错误和一个可选类型的响应。不可否认,我的建议与一般的 Result 类型定义相悖,因为它将失败参数约束为不能符合 ErrorError 协议— (Error, URLResponse?) 。目前 Swift 论坛正在讨论 Error 约束是否有必要。

Result 类型

由于 URLResponse 参数的非直观行为, URLSession 的API 显得特别棘手。但是 Apple 几乎所有的基于回调的异步 API 都具有相同的反模式,它们所提供的类型使得非法状态可以表示。

如何解决这个问题呢?

Swift 的一般方案是定义一个 Result 类型 —一个可以代表通用成功值或错误的枚举。最近,又有人去争取将 Result 添加到标准库

如果 Swift 5 添加了 Result (大胆假设),Apple 可能会自动导入样式如 completionHandler: (A?, Error?) -> Void as (Result<A>) -> Void 的 Cocoa API,将四个可表现的状态转为两个(更大胆的假设)。那时候(如果真的发生的话),我建议你去自己去 实现转换

长时间内,Swift 终有一天将获得对使用异步 API 的适当语言支持。正如无论是社区和 Swift 团队提出的解决方案,都可能允许 移植现有的 Cocoa API 到新的系统中 ,类似于 Objective-C 中 NSError ** 参数是如何作为抛出(throwing)函数导入 Swift。但是别指望在 Swift 6 以前看到这些。


1、你可以自己定义一个 NonZeroInt 类型,但是没有办法告诉编译器“如果有人尝试用零去初始化这个类型,就引发一个错误”。你必须依赖运行时的检查。

不过,引入这样的类型通常是个不错的想法,因为类型的用户可以在初始化之后依赖于所声明的不变性。我还没有在其他地方看到一个 NonZeroInt 类型,保证类型为非空集合的自定义类型更受欢迎。


2、我只是把“ nil ”或“非 nil ”作为可能的状态。显然,非 nil 数据值可以具有无数种可能的状态,并且对于其他两个参数也是如此。但是这些状态对我们来说并不好玩。


以上所述就是小编给大家介绍的《使非法状态不可表示》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

J语言实用教程

J语言实用教程

杨朝杰 / 电子工业出版社 / 1999-04-01 / 30.0

一起来看看 《J语言实用教程》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具