今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

使用@autoclosure来设计Swift API

使用 @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.

感谢阅读!🚀

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。