Selector
@selector
是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL
类型, 它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action
,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。
- (void)callMe {
//...
}
- (void)callMeWithParam:(id)obj {
//...
}
SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);
// 或者也可以使用 `NSSelectorFromString`
//SEL someMethod = NSSelectorFromString(@"callMe");
//SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:");
一般为了方便,很多人会选择使用 @selector
,但是如果要追求灵活的话,可能会更愿意使用 NSSelectorFromString
的版本 -- 因为我们可以在运行时动态生成字符串,从而通过方法的名字来调用到对应的方法。
在 Swift 中没有 @selector
了,取而代之,从 Swift 2.2 开始我们使用 #selector
来从暴露给 Objective-C 的代码中获取一个 selector
。类似地,在 Swift 里对应原来 SEL
的类型是一个叫做 Selector
的结构体。像上面的两个例子在 Swift 中等效的写法是:
func callMe() {
//...
}
func callMeWithParam(obj: AnyObject) {
//...
}
let method = #selector(callMe)
let anotherMethod = #selector(callMeWithParam(obj:))
和 Objective-C 时一样,记得在 callMeWithParam
后面加上冒号和参数名 (:),这才是完整的方法名字。多个参数的方法名也和原来类似,是这个样子:
func turn(by angle: Int, speed: Float) {
//...
}
let method = #selector(turn(by:speed:))
最后需要注意的是,selector 其实是 Objective-C runtime 的概念,如果你的 selector 对应的方法只在 Swift 中可见的话 (也就是说它是一个 Swift 中的 private
方法),在调用这个 selector 时你会遇到一个 unrecognized selector
错误:
// 这是错误代码
private func callMe() {
//...
}
NSTimer.scheduldTimerWithTimeInterval(1, target: self,
selector: #selector(callMe), userInfo: nil, repeays: true)
正确的做法是在 private
前面加上 @objc
关键字,这样运行时就能找到对应的方法了。
@objc private func callMe() {
//...
}
NSTimer.scheduldTimerWithTimeInterval(1, target: self,
selector: #selector(callMe), userInfo: nil, repeays: true)
最后,值得一提的是,如果方法名字在方法所在域内是唯一的话,我们可以简单地只是用方法的名字来作为 #selector
的内容。相比于前面带有冒号的完整的形式来说,这么写起来会方便一些:
let method = #selector(callMe)
let anotherMethod = #selector(callMeWithParam)
let someMethod = #selector(turn)
但是,如果在同一个作用域中存在同样名字的两个方法,即使它们的函数签名不相同,Swift 编译器也不允许编译通过:
func commonFun() {}
func commonFunc(input: Int) -> Int {
return input
}
let method = #selector(commonFunc)
// 编译错误, `commonFunc` 有歧义
对于这种问题,我们可以通过将方法进行强制转换来使用:
let method1 = #selector(commonFunc as () -> ())
let method2 = #selector(commonFunc as (Int) -> Int)
实例方法的动态调用
在 Swift 中有一类很有意思的写法,可以让我们不直接使用实例来调用这个实例上的方法;而是通过类型取出这个类型的某个实例方法的签名,然后通过传递实例来拿到实际需要调用的方法。比如我们有这样的定义:
class MyClass {
func method(_ number: Int) -> Int {
return number + 1
}
}
想要调用 method
的方法的方法的话,最普通的使用方式是生成 MyClass
的实例,然后用 .metnod
来调用它:
//通过创建实例使用实例.语法调用
let object = MyClass()
let number = object.method(1)
这就限定了我们只能够在编译的时候就决定 object
实例和对应的方法调用。其实我们还可以使用刚才说到的方法,将上面的例子改写为:
let f = MyClass.method // f: (MyClass) -> (Int) -> Int
let obj = MyClass()
let result = f(obj)(1)
这种语法看起来会比较奇怪,但是实际上并不复杂。Swift 中可以直接用 Type.instanceMethod
的语法来生成一个可以 柯里化 的方法。如果我们观察 f
的类型 (Alt + 单击),可以知道它是:
f: (MyClass) -> (Int) -> Int
其实对于 Type.instanceMethod
这样的取值语句,实际上刚才
let f = MyClass.method
做的事情类似于下面的字面量转换:
let f = { (obj: MyClass) in obj.method }
这样就不难理解为什么 let result = f(obj)(1)
这样的调用方法可以成立了。这个方法只适用于实例方法,对于属性的 getter 或者 setter 是不能有类似的写法的。另外,当我们遇到方法的名字冲突时:
class MyClass {
func method(_ number: Int) -> Int {
return number + 1
}
//当有类型方法名字冲突时
class func method(number: Int) -> Int {
return number
}
}
如果不加以改动,MyClass.method
抽取到的将是类型方法,如果我们想要取实例方法的话,可以显式地加上类型声明加以区别。这种方式不仅在这里有效,在其他大多数名字有歧义的情况下,都能很好地解决问题:
//当有类型方法名字冲突时 显示的加上类型声明加以区分
//class func method
let f1 = MyClass.method
//class func method 和 f1相同
let f2: (Int) -> Int = MyClass.method
//func method 的 柯里化版本
let f3: (MyClass) -> (Int) -> Int = MyClass.method