SwiftAPI设计之使用@autoclosure

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

Swift的@autoclosure属性能让你定义一个自动被闭包的参数。它的主要作用是推迟表达式(可能代价高昂)的执行,从而避免在传递参数时就直接执行。

assert

在Swift标准库中的assert函数就使用了@autoclosure属性。我们都知道断言只会在debug模式下被触发,因此在release模式下是没有必要执行断言表达式的。assert定义如下

1
2
3
4
5
6
7
8
9
10
11
func assert(_ expression: @autoclosure () -> Bool, 
_ message: @autoclosure () -> String) {
guard isDebug else {
return
}

// 在assert内部,我们可以把表达式当做一个正常的闭包来使用
if !expression() {
assertFailure(message())
}
}

上面是我简单模拟assert的实现,真正的实现在这里

@autoclosure的好处在于它对调用方没有任何影响。如果assert使用正常的闭包来定义的话,那么你就必须这么使用它:

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

但是现在,你可以像调用非闭包参数一样调用它:

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

接下来,让我们来看看如何在自己的代码中使用@autoclosure属性,来使我们的API更友好。

内联赋值

@autoclosure可以在函数调用中内联表达式。我们能利用它做一些事情,比如传递赋值表达式作为参数。我们看看下面这个可能有用的例子。

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

使用@autoclosure,我们可以编写一个自动创建动画闭包并执行它的动画函数,如下所示:

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

现在我们可以使用简单的函数调用来执行动画,而不需要额外的{}语法:

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

使用@autoclosure,我们可以真正减少动画代码的冗长度,而不会牺牲可读性或变现力🎉。

使用表达式传递错误

我发现@autoclosure的另一个非常有用的情况:编写处理错误的代码。比如,假设我们要在Optional上添加一个扩展,使我们能够在解包它出错时抛出异常。这样我们可以要求Optional是非nil,否则将抛出异常。如下所示:

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

类似于assert的实现,我们只会在需要的时候执行表达式,而不是每次尝试解包时都要执行。现在我们可以像这样使用我们的unwrapOrThrow

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

使用默认值做类型推断

我发现的最后的使用场景是从dictionarydatabase或者UserDefaults中提取可选值。

通常,当从一个没有指明特定类型的字典中提取一个值并提供一个默认值时,你必须这么写:

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

这种方式难以阅读,并且有很多复杂的语法糖。使用@autoclosure,我们可以定义一个API,来让我们想下面这样实现同样的功能:

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

从上面,我们可以看到默认值可以拿来做类型推断,而不需要指定类型来完成类型转换。很简洁👍

让我们来看看如何编写这个API:

1
2
3
4
5
6
7
8
9
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来避免每次调用方法是都执行默认值表达式。

结论

减少冗长总是需要仔细考虑的事情。 我们的目标应该始终是编写富有表现力,易于阅读的代码,所以我们需要确保在设计低冗余性API时不会在使用时丢掉重要信息。

我认为在适当的情况下使用@autoclosure是一个很好的工具。用表达式代替数值,使我们能够减少冗长和多余,同时也可能获得更好的性能。

感谢阅读! 🚀