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是一个很好的工具。处理表达式,而不仅仅是值,使我们能够减少冗长和繁琐,同时还可能获得更好的性能。

原文链接