使用@autoclosure来设计Swift API

此文是翻译

原文链接:Using @autoclosure when designing Swift APIs

Swift @autoclosure 属性用于在闭包中定义一个“被包裹”的参数。主要用于延迟执行一段(潜在的耗时、占资源大)代码到真正需要的时候,而不是在参数传递时就执行。

在Swift标准库中就有一个例子,Assert函数的使用。由于asserts仅仅在debug模式下触发,所以没必要在release模式下执行代码。这时就用到了@autoclosure:

1
2
3
4
5
6
7
8
9
10
11
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是用“一般”闭包的方式实现的,那它的使用是这样:

1
assert({ someConfition() }, { "Hey, it failed!" })

但是现在,它可以被当作函数接收无闭包参数来使用:

1
assert(someCondition(), "Hey it failed!")

这周,我们来看一下怎么在我们自己的代码中使用@autoclosure,以及如何使用它设计出优雅的API。

内联函数

@autoclosure的一个作用是在函数调用中内联表达式。这使得我们可以把指定的表达式做为一个参数来使用。我们来看一下下面的例子:

在iOS中,通常使用下面的API来实现view动画:

1
2
3
UIView.animate(withDuration: 0.25) {
view.frame.origin.y = 100
}

使用@autoclosure,我们可以写一个动画函数,自己创建动画的闭包,并且执行。像下面这样:

1
2
3
4
func animate(_ animation: @autoclosure @escaping () -> Void,
duration: TimeInterval = 0.25) {
UIView.animate(withDuration: duration, animations: animation)
}

然后,我们就可以仅仅调用一个简单的函数,没有多余的{},来调用动画

1
animate(view.frame.origin.y = 100)

通过上面的方法,我们减少了冗长的动画代码,且没有丧失可读性。

将错误作为表达式传递

@autoclosure的另一个十分有用的地方是,在写处理错误的工具类的时。例如,我们想要通过throwing API,给Optional添加扩展,来解包它。这种情况下,我们要Optional为非空,或者就throw错误,如下:

1
2
3
4
5
6
7
8
9
extension Optional {
func unwrapThrow(_ errorExpression: @autoclosure() -> Error) {
guard let value = self else {
throw errorExpression()
}

return value
}
}

和assert的实现类似,这样我们判断错误表达式只在需要的时候,而不是每次解包Optional时都判空。我们现在可以如下使用 unwrapOrThrow API:

1
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)

使用默认值进行类型推断

@autoclosure最后一个使用场景是,从dictionary、database、UserDefaults中提取一个可空的值时。

一般情况下,从一个不确定类型的dictionary中获取一个属性,并且提供一个默认值,需要写如下代码:

1
let coins = (dictionay["numberOfCoins"] as? Int) ?? 100

上面的代码不仅难于阅读,而且还有类型推断和??运算符。我们可以使用@autoclosure,定义一个API,来实现同样的表达:

1
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)

如上,我们可以看出,defaultValue一方面作为默认值用于值缺失时,另一方面也用于类型推断,不需要特别声明类型或者类型捕捉。简洁明了👍

然后我们来一下怎么样定义一个这样的API:

1
2
3
4
5
6
7
8
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.

感谢阅读!🚀