在Swift中使用error来控制流程

2021-03-01 12:42:33 浏览数 (1)

此文是翻译

原文链接: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
        ...
    }
}

0 人点赞