此文是翻译
原文链接:Using errors as control flow in Swift
app和项目里管理控制流会对代码的执行速度,代码的调试复杂度有重大的影响。代码的控制流本质上是函数和声明的执行顺序,及代码执行路径。
尽管Swift提供了很多工具定义控制流——例如if, else, while 及 optional;这周,我们来看一下,如何通过Swift编译时错误来抛出和处理model,来让控制流程更容易管理。
抛出可空的值
可选值,作为Swift的重要特征,处理空的数据时可被合法的忽略;它也经常被用作给定函数的来源样板在控制流程中。
下面,重写了从app中bundle加载、调整图片的方法。由于每一步操作都返回了可空的图片,不得不写多个guard语句,告诉函数哪里可以退出:
1 | func loadImage(named name: String, |
上面代码的问题是,我们使用nil来应对运行时错误——这使得我们不得不在每一步都要解析结果,也隐藏了为什么这个错误发生的根本原因。
然后我们来看一下,如何通过抛出函数和错误重构控制流程来解决上面的问题。第一步定义一个包含处理图片的过程中可能出现的所有错误的enum,如下:
1 | enum ImageError: Error { |
然后修改函数失败时抛出上面定义的错误,而不是返回nil。例如,修改loadImage(named:)方法,返回一个非空的image或抛出ImageError.missing:
1 | private func loadImage(named name: String) throws -> UIImage { |
如果把其他的图片处理方法都这样修改了,那顶层的其他函数也可以依葫芦画瓢——移除所有可选,使它们在操作中要不返回确定的图片,要不抛出一个错误:
1 | func loadImage(named name: String, |
上面的修改不仅使函数体更加简洁,也使得调试更加容易,因为如果有错误发生,我们会得到一个明确定义的错误——而不是需要去查哪一步返回的nil。
然而,事实是,并不是所有的地方都需要处理错误,所以不需要强制do、try、catch模式的使用;而且滥用do、try、catch又会导致我们为了尽量避免的样板代码——在用到的时候仔细区分。
好消息是,我们随时可以回去用可空值即使我们用了抛出方法。所需要到只是在调用抛出方法时用try?关键字,然后我们就得到了可选值:
1 | let optionalImage = try? loadImage { |
使用try?最棒的地方是兼具两种方式的优点。既可以在调用中得到个可空值——同时也能用throw、error来管理控制流。
验证输入
接下来,我们来看一下,当验证输入时,使用error如何帮我们提升控制流。尽管Swift有很先进和强大的类型系统,但这并不能保证我们的函数收到合法的输入——有时候运行时检查是唯一的出路。
再看一个例子,用户注册时,验证用户选择到证件。和前面一样,代码用guard语句来判断每个验证规则,如果出错则输入错误信息:
1 | func signUpIfPossible(with credentials: Credentials) { |
尽管上面的代码只校验了两个条件,验证逻辑增长会超出预料。这种逻辑存在于UI中(尤其是view Controller中)会变得更难测试——所以,来看下如何解耦,并且提升代码控制流。
理想状况下,我们希望我们的代码可以自我包含。这样它就可以在隔绝中测试,也可以在我们的代码中使用。为了实现这个,先为所有验证逻辑创建一个指定类型。命名为Validator,是一个结构体,里面是个给定Value的验证闭包:
1 | struct Validator<Value> { |
通过上面的代码,可以构建一个validators,在值验证不通过时,抛出一个错误。然而为每个验证进程都定义新的错误类型也会产生无用的样板(特别是我们想要这些错误展示给用户)——所以,定义一个函数,只需要传Bool的条件和失败时展示给用户的信息的验证代码:
1 | struct ValidationError: LocalizedError { |
上面我们再次用到了@autoclosure——一个自动在闭包内解析的表达式。想要了解更多,查看“Using @autoclosure when designing Swift APIs”.
上面完成之后,就可以写一个指定的眼整齐验证逻辑代码——Validator类型的静态计算属性。例如,下面时一个密码验证器的实现:
1 | extension Validator where Value == String { |
为了做的彻底,重载一个新的validate有点类似语法糖,传入想要验证的值和用于验证的验证器:
1 | func validate<T>(_ value: T, using validator: Validator<T>) throws { |
所有的准备都已经做好,然后用新的验证系统来更新调用。上面代码的优雅之处在于,尽管需要一些额外的类型、额外的设置,但使得需要验证输入的代码更整洁。
1 | func signUpIfPossible(with credentials: Credentials) throws { |