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