使用@autoclosure来设计Swift API
此文是翻译
原文链接:Using @autoclosure when designing Swift APIs
Swift @autoclosure 属性用于在闭包中定义一个“被包裹”的参数。主要用于延迟执行一段(潜在的耗时、占资源大)代码到真正需要的时候,而不是在参数传递时就执行。
在Swift标准库中就有一个例子,Assert函数的使用。由于asserts仅仅在debug模式下触发,所以没必要在release模式下执行代码。这时就用到了@autoclosure:
1 | func assert(_ expression: @autoclosure() -> Bool, |
上面是我理解的assert的实现,assert的真正实现在这里
@autoclosure的一个好处是对于调用位置没有影响。如果assert是用“一般”闭包的方式实现的,那它的使用是这样:
1 | assert({ someConfition() }, { "Hey, it failed!" }) |
但是现在,它可以被当作函数接收无闭包参数来使用:
1 | assert(someCondition(), "Hey it failed!") |
这周,我们来看一下怎么在我们自己的代码中使用@autoclosure,以及如何使用它设计出优雅的API。
内联函数
@autoclosure的一个作用是在函数调用中内联表达式。这使得我们可以把指定的表达式做为一个参数来使用。我们来看一下下面的例子:
在iOS中,通常使用下面的API来实现view动画:
1 | UIView.animate(withDuration: 0.25) { |
使用@autoclosure,我们可以写一个动画函数,自己创建动画的闭包,并且执行。像下面这样:
1 | func animate(_ animation: @autoclosure @escaping () -> Void, |
然后,我们就可以仅仅调用一个简单的函数,没有多余的{},来调用动画:
1 | animate(view.frame.origin.y = 100) |
通过上面的方法,我们减少了冗长的动画代码,且没有丧失可读性。
将错误作为表达式传递
@autoclosure的另一个十分有用的地方是,在写处理错误的工具类的时。例如,我们想要通过throwing API,给Optional添加扩展,来解包它。这种情况下,我们要Optional为非空,或者就throw错误,如下:
1 | extension Optional { |
和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 | extension Dictionary where Value = Any { |
同样的,我们使用@autoclosure避免了每次执行方法的时候都判断defaultValue。
结论
减少冗长代码是需要仔细考虑的。我们的目标是写出能自我解释的、易于阅读的代码,所以在设计低冗余API时,我们需要确定,没有在调用中移除重要信息。
我认为恰当使用@autoclosure,是实现上述目的的伟大工具。处理表达式、不仅仅是值,不仅可以减少冗长的代码,也可以潜在的提高性能。
你认为@autoclosure还有其他使用情景吗?或者有问题、评论、反馈,请联系我。Twitter: @johnsundell.
感谢阅读!🚀