Unwrap or throw: Exploring solutions in Swift
Unwrap或throw 面对这样一种场景:如果一个可选的返回一个空值,我们希望抛出一个错误。像if let或guard语句这样的技术可以很容易地做到这一点,但通常会返回一些样板代码。
在这种情况下,我总是希望找到一个我没有意识到的解决方案。
Unwrap or throw without fancy extensions
在深入研究寻找nil值后抛出错误的奇特解决方案之前,最好先了解如何编写这样的代码解决方案。
假设有一个结构为Uploader(JSON 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代码解决方案。