Swift的@autoclosure属性允许你定义一个参数,该参数自动被封装在一个闭包中。 它主要用于将表达式的执行延迟到实际需要的时候,而不是在传递参数时直接执行。
在Swift标准库中使用该函数的一个例子是assert函数。由于断言只在调试构建中触发,所以不需要计算在发布构建中断言的表达式。这就是@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())
}
}
@autoclosure的好处是它对调用站点没有影响。如果assert是用“普通”闭包实现的,你必须这样使用它:
assert({ someCondition() }, { "Hey, it failed!" })
但是现在,你可以像调用任何带有非闭包参数的函数一样调用它:
assert(someCondition(), "Hey it failed!")
本周,让我们看看如何在我们自己的代码中使用@autoclosure,以及它如何使我们能够设计一些漂亮的api。
Inlining assignments
@autoclosure支持的一件事是在函数调用中内联表达式。这使我们能够做一些事情,比如将赋值表达式作为参数传递。让我们看一个例子,在这个例子中这是很有用的。
在iOS上,你通常使用这个API来定义视图动画:
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)
使用上述技术,我们可以真正减少动画代码的冗长,而不牺牲可读性和表现力🎉
Passing errors as expressions
我发现@autoclosure非常有用的另一种情况是在编写处理错误的实用程序时。 例如,假设我们想在Optional上添加一个扩展,使我们能够使用一个抛出API来展开它。 这样我们就可以要求optional为非nil,或者抛出一个错误,像这样:
extension Optional {
func unwrapOrThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
guard let value = self else {
throw errorExpression()
}
return value
}
}
与实现assert的方式类似,我们只在需要时才计算错误表达式,而不是每次试图打开可选对象时都必须这样做。现在我们可以像这样使用unwrapOrThrow API:
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)
Type inference using default values
我发现的@autoclosure的最后一个用例是在从字典、数据库或UserDefaults中提取可选值时。
通常,当从一个无类型字典中提取一个值并提供一个默认值时,你必须这样写:
let coins = (dictionary["numberOfCoins"] as? Int) ?? 100
这有点难读,并且有很多语法混乱的类型转换和??操作符。通过@autoclosure,我们可以定义一个API,让我们可以像下面这样编写相同的表达式:
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)
在上面,我们可以看到,默认值既用于缺少值时,也允许Swift对该值进行类型推断,而不需要指定类型或执行类型转换。漂亮整洁的👍
让我们看看如何编写这样的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来避免每次调用该方法时都必须计算默认值。
Conclusion
减少冗长总是需要仔细考虑的事情。我们的目标应该始终是编写富有表现力、易于阅读的代码,因此在设计低冗长的api时,我们需要确保不会从调用站点删除重要的信息。
我认为在适当的情况下使用@autoclosure是一个很好的工具。处理表达式,而不仅仅是值,使我们能够减少冗长和繁琐,同时还可能获得更好的性能。