WWDC 20 前你应该知道的 Swift 新特性:callAsFunction

3,364 阅读3分钟

乡亲们我回来了

一年写一次,一次写一批,一批写完后休息一年,在 WWDC 20 前又回来了,因为又有新素材了嘛。Swift 在 5.1 之后逐渐进入成熟期,所以 Swift 5.2 中的语言新特性多数是小修小补,既然这些特性在最新的 Xcode 正式版已经可以用了,我们不妨一起了解一下吧。本期主题:callAsFunction

遥想当年

遥想当年在大一的 C++ 课堂上老师介绍的 function call operator,哇,真是神奇,一个对象可以当成函数那样调用呢!它还有一个好听?的名字叫做函数对象,也有的叫做函数子(functor)。先来一段 C++ 回味一下。

struct Adder
{
public:
  Adder(int base):m_base(base){}
  int operator()(int a) {
    return m_base + a;
  }
private:
  int m_base;
};

int main(int argc, const char * argv[]) {
  int adder = Adder(2);
  cout << adder(6) << endl; //8
  return 0;
}

函数就是函数,为什么需要搞得那么复杂呢?原因是与普通的函数不同, Adder 是带状态的。后来的 C++ 11 中引入了 lambda 特性(类似于 Swift 的 closure),它的一种常见实现方式,就是编译成上面的函数对象。

上世纪的特性

如同 C++ 98 中的函数对象,Swift 5.2 中的 callAsFunction是个彻头彻尾的上世纪特性。Kotlin 中也有类似的 invoke operator,所以对 Swift 来说不是啥发明,只是下定决心把这个小小语法糖给加上了。我们来看一下 Swift 的版本:

struct Adder {
    var base: Int
    func callAsFunction(_ x: Int) -> Int {
        return base + x
    }
}

let add2 = Adder(base: 2)
print(add2(6)) // 8

这里的 callAsFunction(...)函数,就是Swift 编译器约定的名字,这个函数可以有 0-N 个参数,甚至不定参也可以的。除了函数名字的约定,基本上没有任何可以限制你发挥的地方,你可以加上泛型,甚至可以用 class 或者 enum,当然这种对象一般是值类型语义,所以类型定义成struct是最常见的,其次是enum也可以的,而定义成class 的话一要记得引用类型的开销,二是必须给它添加init构造函数。

好学的宝宝可能要问为什么上面 C++ 的例子不可以是class呢?其实,在 C++ 的例子中使用 class 或者 struct 是一样的,C++ 中 classstruct 的最主要的区别在于成员的默认访问级别是什么,而在上面的例子中我们指定了访问级别。在 C++ 中,对象构造在堆上还是栈上是使用者控制的,这点与 Swift 是不同的。

下面的这个例子显示,callAsFunction仅仅是一个语法糖,可以直接用这个名字进行调用。

let add2 = Adder(base: 2)
print(add2(6)) // 8
print(add2.callAsFunction(6)) // 8

Swift Callables

在 Swift 中这种不是函数,但可以当成函数一样调用对象还有哪些呢?我们来盘点一下:

  1. 函数类型的值,在 Swift 中函数是一等公民,有类型,有值。所以callAsFunction也可以被赋值给函数类型,供之后调用,这不是针对callAsFunction的新特性,对于其他名称的方法也可以这样。
let f: (Int) -> Int = add2.callAsFunction(_:)
print(f(6)) // 8
  1. 类型名称,我们经常使用 UIView(...) 形式来构造对象,但实际上这个语法糖会被转换成函数调用 UIView.init(...)

  2. @dynamicCallable 修饰的类型,多用于与动态语言的交互。

@dynamicCallable struct DummyCallable {
    func dynamicallyCall(withArguments: [Int]) {
      print(withArguments)
  }
    func dynamicallyCall(withKeywordArguments: KeyValuePairs<String, Int>) {
      print(withKeywordArguments)
  }
}

let x = DummyCallable()
x(1,2,3) // [1, 2, 3]
x(Leon: 28, 178) // ["Leon": 28, "": 178]

从某种程度上来讲,callAsFunction 可以看作是这种形式的静态小伙伴。

扫码下方二维码,关注“面试官小健”