Unwrap or throw: Exploring solutions in Swift

Unwrap或throw 面对这样一种场景:如果一个可选的返回一个空值,我们希望抛出一个错误。像if letguard语句这样的技术可以很容易地做到这一点,但通常会返回一些样板代码。

在这种情况下,我总是希望找到一个我没有意识到的解决方案。

Unwrap or throw without fancy extensions

在深入研究寻找nil值后抛出错误的奇特解决方案之前,最好先了解如何编写这样的代码解决方案。

假设有一个结构为UploaderJSON Web令牌(JWT)作为其输入)定义上传输入。JWT可以包含几个声明,我们需要对这些声明进行解码,以找到我们的上传输入值。当一个值不存在时,我们希望抛出一个详细的错误。代码如下所示:

struct UploadInput {
    
    enum UploadInputError: Error {
        case invalidJWT
        case missingIdentifier
        case missingUploadURL
    }
    
    let uploadIdentifier: String
    let uploadURL: URL
    
    init(token: JWT) throws {
        guard let decodedToken = JWTDecode.decode(jwt: token) else {
            throw UploadInputError.invalidJWT
        }
        guard let uploadIdentifier = decodedToken.claim(name: "upload.id").string else {
            throw UploadInputError.missingIdentifier
        }
        guard let uploadURL = decodedToken.claim(name: "upload.url").url else {
            throw UploadInputError.missingUploadURL
        }
        self.uploadIdentifier = uploadIdentifier
        self.uploadURL = uploadURL
    }
}

上面示例代码的初始化式是易读和易理解的。然而,如果我们添加更多带有详细错误的拆封,我们可能会很容易地得到一个大的初始化式,从而降低了可读性。

通过直接将未包装的值赋给实例属性,或者立即抛出一个错误,来改进上述代码示例将非常好。让我们探索如何通过实现unwrap或throw的解决方案来解决这个问题。

Exploring solutions for throwing an error on nil

Swift使我们能够编写多个解决方案来展开包装,或者在发现空值时抛出错误。 如前所述,下面的代码示例受到了这个Swift论坛帖子的启发,如果您想进一步探讨这个主题,这个帖子可能是一个鼓舞人心的地方。

在下面的代码解决方案中,我们将放大上述代码示例的初始化式,并展示使用给定优化的最终结果。

Using a closure

第一个解决方案已经展示了我们如何在初始化式中最小化代码,通过编写一个更聪明的解决方案,避免在发现nil值时抛出错误:

init(token: JWT) throws {
    let decodedToken = try JWTDecode.decode(jwt: token) ?? { throw UploadInputError.invalidJWT }()
    uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?? { throw UploadInputError.missingIdentifier }()
    uploadURL = try decodedToken.claim(name: "upload.url").url ?? { throw UploadInputError.missingUploadURL }()
}

然而,在这里使用闭包并不能提高可读性。

Using a method to throw an error

通过编写泛型方法,我们可以替换上面的闭包,使它更具可读性:

func throwError<T>(_ error: Error) throws -> T {
    throw error
}

init(token: JWT) throws {
    let decodedToken = try JWTDecode.decode(jwt: token) ?? throwError(UploadInputError.invalidJWT)
    uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?? throwError(UploadInputError.missingIdentifier)
    uploadURL = try decodedToken.claim(name: "upload.url").url ?? throwError(UploadInputError.missingUploadURL)
}

这个代码示例比闭包示例提高了可读性,并且可以很好地替代我们的初始化式。然而,在我寻找最佳解决方案的过程中,我希望找到一个自定义操作符来展开或抛出。最初被拒绝的提议包括这样一个操作符,所以让我们看看今天是否可以复制这个操作符。

Using a custom nil coalescing operator

并不是所有人都喜欢在Swift中使用自定义操作符。它们可以紧凑和方便,但在新代码库中很难找到。在这种情况下,你是否喜欢操作员取决于你自己。与你分享我的观点:我喜欢使用它们!然而,在我们的项目中实现这一点之前,我也必须与我的同事达成一致。

让我们深入代码示例,并探讨自定义操作符如何改进初始化式来展开或抛出。首先,我们必须定义自定义的nil合并操作符:

infix operator ?!: NilCoalescingPrecedence

/// Throws the right hand side error if the left hand side optional is `nil`.
func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
    guard let value = value else {
        throw error()
    }
    return value
}

该操作符方法使用了与泛型相结合的自动闭包。在初始化式中使用它会得到目前为止最小的结果:

init(operator token: JWT) throws {
    let decodedToken = try JWTDecode.decode(jwt: token) ?! UploadInputError.invalidJWT
    uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?! UploadInputError.missingIdentifier
    uploadURL = try decodedToken.claim(name: "upload.url").url ?! UploadInputError.missingUploadURL
}

就我个人而言,我最喜欢这个解决方案。学习新的?!操作符也有坏处,但在我看来,结果是最紧凑但可读的代码。

Conclusion

在Swift中探索代码解决方案很有趣,也会让你成为一名更好的工程师。您将发现其他人编写的解决方案,这些解决方案将激励您改进代码。即使您最终没有使用任何其他已找到的解决方案,您仍然了解了许多关于在Swift中可用的unwrapping或throw代码解决方案。