由donnywals于2020年2月17日发布
Swift 5.2的新功能是能够将类型的实例作为函数来调用。 或者,如Swift Evolution提案所称,它是“用户定义的标称类型的可调用值”。 此功能的简短描述是,它允许您调用实现了callAsFunction方法的任何类型的实例,就好像它是一个函数一样:
struct InvestmentsCalculator {
let input: Double
let averageGrowthPerYear = 0.07
func callAsFunction(years: Int) -> Double {
return (0..<years).reduce(input, { value, _ in
return value * (1 + averageGrowthPerYear)
})
}
}
let calculator = InvestmentsCalculator(input: 1000)
let newValue = calculator(years: 10)
尽管这很酷,但您可能想知道这样的功能在现实世界中何时有用,以及如何在代码中应用它。 幸运的是,这正是我希望能帮助您在今天的帖子中发现的东西,也是我一直试图发现自己的东西。
Understanding the background of callAsFunction
Swift不是唯一允许其用户将某些类型的实例作为函数调用的语言。 另一种允许这种行为的语言是Python。 在某些您想要表示状态计算器,解析器或计算对象的应用程序中,将实例作为函数调用的功能非常有趣。 这在复杂的数学运算和机器学习中非常常见,其中某些对象可能会保持某种状态并且仅实现单个方法。
我自己比较熟悉的应用程序是将渲染器对象传递给Python中的函数。 在这篇文章中,我不会让您使用Python代码,但是我想向您展示我之前的声明的意思。 因此,让我们看一个Swift示例:
protocol Route {
associatedtype Output
}
func registerHandler<R: Route>(_ route: R, _ handler: (R) -> R.Output) {
return renderer(route)
}
这看起来应该不会太疯狂。 它是一个函数,它接受符合Route的对象,并接受一个以该Route为参数的闭包,并返回R.Output(无论如何)。 如果要调用此函数,则可以编写以下内容:
registerHandler(homeRoute) { route in
/* Do a bunch of work to create output */
return output
}
对于非常简单的渲染器,这可能会很好地工作。 但是,在我之前提到的Python上下文中,这种代码将在Web服务器上运行。 路由将等同于URL或网页路径。 每当用户请求关联的路由时,都会调用传递给registerHandler的闭包。 在许多情况下,简单的闭包是行不通的。 最终处理路由的对象将需要具有数据库连接,缓存,身份验证以及可能的许多其他功能的概念。 将所有这些捕获在一个闭包中似乎并不是一个好主意。 相反,您希望某种复杂的对象来处理此路由。 这正是我们使用callAsFunction获得的那种自由:
struct HomeHandler {
let database: Database
let authenticator: Authenticator
// etc...
func callAsFunction<R: Route>(_ route: R) -> R.Output {
/* Do a bunch of work to create output */
return output
}
}
let homeHander = HomeHandler(database: database, authenticator: authenticator)
registerHandler(for: homeRoute, homeHandler)
像这样的模式在Python中很常见,我认为能够透明地将整个对象传递给带闭包的函数真的很酷。
如果您对闭包有所了解,那么您可能想知道前面的代码与以下代码之间是否有任何区别:
registerHandler(for: homeRoute, homeHandler.callAsFunction)
实际上,除了您需要进行的键入数量之外,没有什么区别。 函数仍然可以传递给使用闭包的函数。 它读起来更好一些,而不必指定需要精确调用对象上的哪个函数。
Understanding how to use callAsFunction
在Swift中使用callAsFunction相对简单。 任何定义了callAsFunction方法的对象都可以视为一个函数。 您的callAsFunction可以接受参数并返回值,如Swift Evolution建议中所示,并带有以下示例:
struct Adder {
let base: Int
func callAsFunction(_ x: Int) -> Int {
return base + x
}
}
let add3 = Adder(base: 3)
add3(10) // 13
或完全不接受任何参数:
struct Randomizer<T> {
let elements: [T]
func callAsFunction() -> T? {
return elements.randomElement()
}
}
let randomizer = Randomizer(elements: [1, 2, 3])
randomizer() // a random element
您甚至可以在单个对象上具有多个重载:
struct Randomizer<T> {
let elements: [T]
func callAsFunction() -> T? {
return elements.randomElement()
}
func callAsFunction(resultCount: Int) -> [T] {
return (0..<resultCount).reduce(into: [T]()) { result, _ in
if let element = elements.randomElement() {
result.append(element)
}
}
}
}
let randomizer = Randomizer(elements: [1, 2, 3])
randomizer() // a random element
randomizer(resultCount: 2) // an array with two random elements
决定是否要让callAsFunction实现接受参数以及返回类型是什么的能力使其成为一个非常强大的功能。 您确实可以根据自己的需求自定义此功能,并且由于可以向对象添加多个callAsFunction重载,因此可以在多个上下文中将单个对象用作函数。
总结
使您的类型可调用的功能可能不是您经常使用的功能。 这是一个利基功能,具有几个非常特定但方便的应用程序。 在今天的帖子中,您看到了一个示例,该示例说明了Python如何在我几年前与之合作建立网站的框架中使用可调用对象。 我将使用对象注册路由处理程序的功能大致转换为Swift,以使您了解实际应用程序的外观,并从新的角度为您提供一些背景知识。
然后我继续向您展示如何在自己的类型上使用callAsFunction。 您看到完全由您决定是否要让实现接受参数,并自行选择callAsFunction的返回类型。 您还了解到可以为callAsFunction指定重载,这意味着您可以在单个类型上具有多种形式的callAsFunction。
译自:https://www.donnywals.com/how-and-when-to-use-callasfunction-in-swift-5-2/