使用 @autoclosure 来设计 Swift API#
此文是翻译#
原文链接:Using @autoclosure when designing Swift APIs
Swift @autoclosure 属性用于在闭包中定义一个 “被包裹” 的参数。主要用于延迟执行一段(潜在的耗时、占资源大)代码到真正需要的时候,而不是在参数传递时就执行。
在 Swift 标准库中就有一个例子,Assert 函数的使用。由于 asserts 仅仅在 debug 模式下触发,所以没必要在 release 模式下执行代码。这时就用到了 @autoclosure:
func assert(_ expression: @autoclosure() -> Bool,
_ message: @autoclosure() -> String) {
guard isDebug else {
return
}
// Inside assert we can refer to expression as a normal closure
if !expression() {
assertionFailure(message())
}
}
上面是我理解的 assert 的实现,assert 的真正实现在这里
@autoclosure 的一个好处是对于调用位置没有影响。如果 assert 是用 “一般” 闭包的方式实现的,那它的使用是这样:
assert({ someConfition() }, { "Hey, it failed!" })
但是现在,它可以被当作函数接收无闭包参数来使用:
assert(someCondition(), "Hey it failed!")
这周,我们来看一下怎么在我们自己的代码中使用 @autoclosure,以及如何使用它设计出优雅的 API。
内联函数#
@autoclosure 的一个作用是在函数调用中内联表达式。这使得我们可以把指定的表达式做为一个参数来使用。我们来看一下下面的例子:
在 iOS 中,通常使用下面的 API 来实现 view 动画:
UIView.animate(withDuration: 0.25) {
view.frame.origin.y = 100
}
使用 @autoclosure,我们可以写一个动画函数,自己创建动画的闭包,并且执行。像下面这样:
func animate(_ animation: @autoclosure @escaping () -> Void,
duration: TimeInterval = 0.25) {
UIView.animate(withDuration: duration, animations: animation)
}
然后,我们就可以仅仅调用一个简单的函数,没有多余的 {},来调用动画:
animate(view.frame.origin.y = 100)
通过上面的方法,我们减少了冗长的动画代码,且没有丧失可读性。
将错误作为表达式传递#
@autoclosure 的另一个十分有用的地方是,在写处理错误的工具类的时。例如,我们想要通过 throwing API,给 Optional 添加扩展,来解包它。这种情况下,我们要 Optional 为非空,或者就 throw 错误,如下:
extension Optional {
func unwrapThrow(_ errorExpression: @autoclosure() -> Error) {
guard let value = self else {
throw errorExpression()
}
return value
}
}
和 assert 的实现类似,这样我们判断错误表达式只在需要的时候,而不是每次解包 Optional时都判空。我们现在可以如下使用 unwrapOrThrow API:
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)
使用默认值进行类型推断#
@autoclosure 最后一个使用场景是,从 dictionary、database、UserDefaults 中提取一个可空的值时。
一般情况下,从一个不确定类型的 dictionary 中获取一个属性,并且提供一个默认值,需要写如下代码:
let coins = (dictionay["numberOfCoins"] as? Int) ?? 100
上面的代码不仅难于阅读,而且还有类型推断和??运算符。我们可以使用 @autoclosure,定义一个 API,来实现同样的表达:
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)
如上,我们可以看出,defaultValue 一方面作为默认值用于值缺失时,另一方面也用于类型推断,不需要特别声明类型或者类型捕捉。简洁明了👍
然后我们来一下怎么样定义一个这样的 API:
extension Dictionary where Value = Any {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
guard let value = self[key] as? T else {
return defaultValue()
}
return value
}
}
同样的,我们使用 @autoclosure 避免了每次执行方法的时候都判断 defaultValue。
结论#
减少冗长代码是需要仔细考虑的。我们的目标是写出能自我解释的、易于阅读的代码,所以在设计低冗余 API 时,我们需要确定,没有在调用中移除重要信息。
我认为恰当使用 @autoclosure,是实现上述目的的伟大工具。处理表达式、不仅仅是值,不仅可以减少冗长的代码,也可以潜在的提高性能。
你认为 @autoclosure 还有其他使用情景吗?或者有问题、评论、反馈,请联系我。Twitter: @johnsundell.
感谢阅读!🚀