来自 Apple 的文档:
该
@dynamicCallable
属性使您可以named types
像调用函数一样使用简单的语法糖进行调用。主要用例是 动态语言互操作性。
为什么要使用@dynamicCallable
而不是直接方法?
@dynamicCallable
是 Swift 5 的一个新特性。来自Paul Hudson 关于“如何在 Swift 中使用 @dynamicCallable”的文章(强调我的):
SE-0216为 Swift添加了一个新
@dynamicCallable
属性,它带来了将类型标记为可直接调用的能力。它是语法糖而不是任何类型的编译器魔法, 有效地转换了这段代码:let result = random(numberOfZeroes: 3)
进入这个:
let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])
[...]
@dynamicCallable
是@dynamicMemberLookup
[ SE-0195 ] 的自然扩展,目的相同:让 Swift 代码更容易与 Python 和 JavaScript 等动态语言一起工作。[...]@dynamicCallable
对于其方法接受和返回的数据类型非常灵活,允许您从 Swift 的所有类型安全中受益,同时仍有一些用于高级使用的回旋余地。
这个提议是对SE-0195 - 引入用户定义的“动态成员查找”类型的后续,它在 Swift 4.2 中提供。它引入了一个新@dynamicCallable
属性,该属性将类型标记为使用正常语法“可调用”。它是简单的语法糖,允许用户编写:
a = someValue(keyword1: 42, "foo", keyword2: 19)
并让它由编译器重写为:
a = someValue.dynamicallyCall(withKeywordArguments: [
"keyword1": 42, "": "foo", "keyword2": 19
])
许多其他语言具有类似的特性(例如 Python “callables”、C++operator()
和
许多其他语言中的函子),但该提案的主要动机是允许在 Swift 中与动态语言进行优雅和自然的互操作。
Swift-evolution 线程: - Pitch:引入用户定义的动态“可调用”类型。-推介#2:引入用户定义的动态“可调用”类型。- 当前螺距螺纹:Pitch #3:引入用户定义的动态“可调用”类型
Swift 在与现有 C 和 Objective-C API 互操作方面表现出色,我们希望将这种互操作性扩展到 Python、JavaScript、Perl 和 Ruby 等动态语言。我们在漫长的设计过程中探索了这个总体目标,其中 Swift 进化社区评估了多种不同的实现方法。结论是,最好的方法是将大部分复杂性放入编写为纯 Swift 库的动态语言特定绑定中,但在 Swift 中添加小钩子以允许这些绑定为其客户提供自然体验。 SE-0195 是这个过程的第一步,它引入了一个绑定,以在动态语言中自然地表达成员查找规则。
与 Python 的互操作性是什么意思?让我们通过一个例子来解释这一点。下面是一些简单的 Python 代码:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each `Dog`
def add_trick(self, trick):
self.tricks.append(trick)
借助 Swift 4.2 中引入的SE-0195@dynamicMemberLookup
功能
,可以实现用 Swift 编写的Python 互操作层
。它与 Python 运行时互操作,并将所有 Python 值投影到单一PythonObject
类型中。它允许我们像这样调用
Dog
类:
// import DogModule.Dog as Dog
let Dog = Python.import.call(with: "DogModule.Dog")
// dog = Dog("Brianna")
let dog = Dog.call(with: "Brianna")
// dog.add_trick("Roll over")
dog.add_trick.call(with: "Roll over")
// dog2 = Dog("Kaylee").add_trick("snore")
let dog2 = Dog.call(with: "Kaylee").add_trick.call(with: "snore")
这也适用于任意其他 API。这是一个使用 Python pickle
API 和内置 Python 函数的示例open
。请注意,我们选择将内置 Python 函数(如import
和)open
放入
Python
命名空间以避免污染全局命名空间,但其他设计也是可能的:
// import pickle
let pickle = Python.import.call(with: "pickle")
// file = open(filename)
let file = Python.open.call(with: filename)
// blob = file.read()
let blob = file.read.call()
// result = pickle.loads(blob)
let result = pickle.loads.call(with: blob)
这种能力运作良好,但不得不使用
foo.call(with: bar, baz)
代替的语法负担foo(bar, baz)
是显着的。除了语法的重要性之外,它还直接损害了代码的清晰度,使代码难以阅读和理解,违背了 Swift 的核心价值。
提出的@dynamicCallable
属性直接解决了这个问题。有了它,这些例子变得更加自然和清晰,在表现力上有效地匹配了原始 Python 代码:
// import DogModule.Dog as Dog
let Dog = Python.import("DogModule.Dog")
// dog = Dog("Brianna")
let dog = Dog("Brianna")
// dog.add_trick("Roll over")
dog.add_trick("Roll over")
// dog2 = Dog("Kaylee").add_trick("snore")
let dog2 = Dog("Kaylee").add_trick("snore")
Python 内建函数:
// import pickle
let pickle = Python.import("pickle")
// file = open(filename)
let file = Python.open(filename)
// blob = file.read()
let blob = file.read()
// result = pickle.loads(blob)
let result = pickle.loads(blob)
这个提议只是引入了一个语法糖——它没有向 Swift 添加任何新的语义模型。我们相信,与脚本语言的互操作性是 Swift 社区的一个重要且不断增长的需求,尤其是当 Swift 进入服务器开发和机器学习社区时。这个特性在其他语言中也有先例(例如 Scala 的
Dynamic
trait),并且可以用于除语言互操作性之外的其他目的(例如实现动态代理对象)。
我们建议为 Swift 语言引入一个新@dynamicCallable
属性,该属性可应用于结构、类、枚举和协议。这遵循了
SE-0195的先例。
在这个提议之前,这些类型的值在调用表达式中是无效的:Swift 中唯一存在的可调用值是那些具有函数类型(函数、方法、闭包等)和元类型(它们是初始化表达式,如String(42)
)的值。因此,“调用”名义类型的实例(例如结构)总是错误的。
有了这个提议,@dynamicCallable
在其主要类型声明中具有属性的类型变为“可调用的”。他们需要至少实现以下两种处理调用行为的方法之一:
func dynamicallyCall(withArguments: <#Arguments#>) -> <#R1#>
// `<#Arguments#>` can be any type that conforms to `ExpressibleByArrayLiteral`.
// `<#Arguments#>.ArrayLiteralElement` and the result type `<#R1#>` can be arbitrary.
func dynamicallyCall(withKeywordArguments: <#KeywordArguments#>) -> <#R2#>
// `<#KeywordArguments#>` can be any type that conforms to `ExpressibleByDictionaryLiteral`.
// `<#KeywordArguments#>.Key` must be a type that conforms to `ExpressibleByStringLiteral`.
// `<#KeywordArguments#>.Value` and the result type `<#R2#>` can be arbitrary.
// Note: in these type signatures, bracketed types like <#Arguments#> and <#KeywordArguments#>
// are not actual types, but rather any actual type that meets the specified conditions.
如上所述,<#Arguments#>
并且<#KeywordArguments#>
可以是分别符合
ExpressibleByArrayLiteral
和
ExpressibleByDictionaryLiteral
协议的任何类型。后者包含
KeyValuePairs
,它支持重复键,不像Dictionary
. 因此,KeyValuePairs
建议使用 using 来支持重复的关键字和位置参数(因为位置参数被脱糖为关键字参数,以空字符串""
为键)。
如果一个类型实现了该withKeywordArguments:
方法,则可以使用位置参数和关键字参数动态调用它:位置参数以空字符串""
作为键。如果一个类型只实现了该
withArguments:
方法但使用关键字参数调用,则会发出编译时错误。
由于动态调用是直接调用方法的语法糖,因此直接转发方法的dynamicallyCall
附加行为。dynamicallyCall
例如,如果一个dynamicallyCall
方法用throws
or标记@discardableResult
,那么相应的加糖动态调用将转发该行为。
由于有两种@dynamicCallable
方法,可能有多种方法来处理一些动态调用。如果一个类型同时指定了
withArguments:
和withKeywordArguments:
方法会发生什么?
我们建议类型检查器根据表达式的句法形式解决这种歧义,以实现最紧密的匹配。确切的规则是:
@dynamicCallable
类型实现了该withArguments:
方法并且在没有关键字参数的情况下调用它,请使用该withArguments:
方法。withKeywordArguments:
方法。
@dynamicCallable
类型实现该
withKeywordArguments:
方法并使用至少一个关键字参数调用它的情况。@dynamicCallable
类型仅实现withKeywordArguments:
方法(而不是withArguments:
方法)并且在没有关键字参数的情况下调用它的情况。@dynamicCallable
type 未实现该withKeywordArguments:
方法但调用站点具有关键字参数,则会发出错误。以下是一些玩具说明性示例:
@dynamicCallable
struct Callable {
func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count }
}
let c1 = Callable()
c1() // desugars to `c1.dynamicallyCall(withArguments: [])`
c1(1, 2) // desugars to `c1.dynamicallyCall(withArguments: [1, 2])`
c1(a: 1, 2) // error: `Callable` does not define the 'withKeywordArguments:' method
@dynamicCallable
struct KeywordCallable {
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
return args.count
}
}
let c2 = KeywordCallable()
c2() // desugars to `c2.dynamicallyCall(withKeywordArguments: [:])`
c2(1, 2) // desugars to `c2.dynamicallyCall(withKeywordArguments: ["": 1, "": 2])`
c2(a: 1, 2) // desugars to `c2.dynamicallyCall(withKeywordArguments: ["a": 1, "": 2])`
@dynamicCallable
struct BothCallable {
func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count }
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
return args.count
}
}
let c3 = BothCallable()
c3() // desugars to `c3.dynamicallyCall(withArguments: [])`
c3(1, 2) // desugars to `c3.dynamicallyCall(withArguments: [1, 2])`
c3(a: 1, 2) // desugars to `c3.dynamicallyCall(withKeywordArguments: ["a": 1, "": 2])`
考虑到 Swift 类型检查器的行为,这种歧义解析规则很自然地起作用,因为它仅在知道基本表达式的类型时才解析调用表达式。此时,它知道基类是函数类型、元类型还是有效@dynamicCallable
类型,并且知道调用的句法形式。
该提议不需要对约束求解器进行大规模或侵入性更改。请查看实现以获取更多详细信息。
在这里,我们绘制了一些示例绑定来展示如何在实践中使用它。请注意,有许多设计决策与此提案正交(例如如何处理异常),我们不在这里讨论。这只是为了展示此功能如何提供语言绑定作者可以用来实现其所需结果的基础设施。这些示例还展示@dynamicMemberLookup
了它们如何协同工作,但省略了其他实现细节。
JavaScript 支持可调用对象,但没有关键字参数。
这是一个示例 JavaScript 绑定:
@dynamicCallable @dynamicMemberLookup
struct JSValue {
// JavaScript doesn't have keyword arguments.
@discardableResult
func dynamicallyCall(withArguments: [JSValue]) -> JSValue { ... }
// This is a `@dynamicMemberLookup` requirement.
subscript(dynamicMember member: JSValue) -> JSValue {...}
// ... other stuff ...
}
另一方面,一种常见的 JavaScript 模式是将值字典作为参数标签的替代(
example({first: 1, second: 2, third: 3})
在 JavaScript 中称为)。Swift 中的 JavaScript 桥可以选择实现关键字参数支持,以允许example(first: 1, second: 2, third: 3)
从 Swift 代码中调用它(感谢 Ben Rimmington 的观察)。
Python 确实支持关键字参数。虽然 Python 绑定只能实现该withKeywordArguments:
方法,但最好同时实现非关键字和关键字形式,以使非关键字大小写稍微更有效(避免分配临时存储)并更好地使用位置参数进行直接调用(x.dynamicallyCall(withArguments: 1, 2)
而不是
x.dynamicallyCall(withKeywordArguments: ["": 1, "": 2])
)。
这是一个示例 Python 绑定:
@dynamicCallable @dynamicMemberLookup
struct PythonObject {
// Python supports arbitrary mixes of keyword arguments and non-keyword
// arguments.
@discardableResult
func dynamicallyCall(
withKeywordArguments: KeyValuePairs<String, PythonObject>
) -> PythonObject { ... }
// An implementation of a Python binding could choose to implement this
// method as well, avoiding allocation of a temporary array.
@discardableResult
func dynamicallyCall(withArguments: [PythonObject]) -> PythonObject { ... }
// This is a `@dynamicMemberLookup` requirement.
subscript(dynamicMember member: String) -> PythonObject {...}
// ... other stuff ...
}
按照 SE-0195 的先例,该属性必须放在类型的主要定义上,而不是扩展上。
该提案没有引入提供动态可调用
static
/class
成员的能力。鉴于支持像 Python 这样的动态语言的目标,我们认为这并不重要,但如果将来发现用例,可以探索它。这种未来的工作应该记住,元类型上的调用语法已经很有意义,并且必须以某种方式解决歧义(例如通过最具体的规则)。
这个提议支持直接调用值和方法,但是在 Smalltalk 家族语言中支持柯里化方法的子集。鉴于 Swift 编译器中当前的柯里化状态,这只是一个实现限制。如果有特定需要,将来可以添加支持。
这是一个严格的附加提案,没有源中断更改。
这是一个严格的附加提案,没有 ABI 重大更改。
这对其他语言功能尚未捕获的 API 弹性没有影响。
除了支持 Python 和 JavaScript 等语言外,我们还希望能够支持 Smalltalk 派生语言,如 Ruby 和 Squeak。这些语言同时使用基本名称和关键字参数来解析方法调用。例如,考虑以下 Ruby 代码:
time = Time.zone.parse(user_time)
Time.zone
引用是一个成员查找,但它zone.parse(user_time)
是一个方法调用,需要与查找zone.parse
后跟直接函数调用不同的处理方式。
这可以通过添加一个新@dynamicMemberCallable
属性来处理,该属性的作用类似于@dynamicCallable
但启用动态成员调用(而不是动态调用self
)。
@dynamicMemberCallable
会有以下要求:
func dynamicallyCallMethod(named: S1, withArguments: [T5]) -> T6
func dynamicallyCallMethod(named: S2, withKeywordArguments: [S3 : T7]) -> T8
这是一个示例 Ruby 绑定:
@dynamicMemberCallable @dynamicMemberLookup
struct RubyObject {
@discardableResult
func dynamicallyCallMethod(
named: String, withKeywordArguments: KeyValuePairs<String, RubyObject>
) -> RubyObject { ... }
// This is a `@dynamicMemberLookup` requirement.
subscript(dynamicMember member: String) -> RubyObject {...}
// ... other stuff ...
}
该提案主要针对动态语言互操作性。对于这个用例,该方法采用可变大小的参数列表是有意义的,dynamicallyCall
其中每个参数都具有相同的类型。但是,支持一般可调用行为(类似于
operator()
在 C++ 中)可能很有用,其中去糖的“可调用”方法可以具有固定数量的参数和不同类型的参数。
例如,考虑以下内容:
struct BinaryFunction<T1, T2, U> {
func call(_ argument1: T1, _ argument1: T2) -> U { ... }
}
展望支持糖化此类事物的那一天并非不合理,尤其是当/如果 Swift 获得可变参数泛型时。这可以允许类型安全的 n 元智能函数指针类型。
我们认为本提案中概述的方法支持这一方向。当/如果出现一般可调用行为的激励用例时,我们可以简单地添加一个新表单来表示它并增强类型检查器以在歧义解决期间更喜欢它。如果这是一个可能的方向,那么最好命名属性@callable
而不是@dynamicCallable
预期未来的增长。
我们认为一般的可调用行为@dynamicCallable
是正交特征,应该单独评估。
考虑并讨论了许多替代方案。它们中的大多数都在SE-0195 的“考虑的替代方案”部分中捕获。
以下是讨论中提出的几点:
有人建议我们使用下标来表示调用实现而不是函数调用,与
@dynamicMemberLookup
. 我们认为函数更适合这里:@dynamicMemberLookup
使用下标的原因是允许成员是左值,但调用结果不是左值。
要求我们结合此处提出的动态版本设计和实施此提案的“静态可调用”版本。在作者看来,将静态可调用支持视为未来可能的方向很重要,以确保这两个功能彼此相邻并具有一致的设计(我们相信这个提议已经做到了),但事实并非如此加入这两个提案是有意义的。到目前为止,还没有为静态可调用版本提供强烈的激励用例,并且 Swift 缺乏使静态可调用通用化所必需的某些泛型特性(例如可变参数)。我们认为静态可调用对象应该独立于其自身的优点。