选择器
此文章翻译自苹果官方文档原文地址:http://developer.apple.com/TP30001163-CH12-SW1
选择器
在objc中选择符有两个含义。一种是用在代码中向对象发送消息时它代表了一个方法名。另一种是当源代码被编译时选择器会指向一个唯一标识以代替方法名,被编译后的选择器类型为SEL。所有相同名字的方法会有相同的选择器。你可以使用一个选择器来调用一个对象的方法。这个是Cocoa中目标-动作设计模式的基础。
方法和选择器
出于运行效率的考虑,在编译后的代码中不会使用由ASCII码组成的方法名。编译器会将每个方法名写到一个表中,然后为每个方法名分配一个唯一标识用于在运行时标识一个方法。运行时系统会确保每个标识都是唯一:不会出现两个相同的选择器,并且所有相同名字的方法都使用同一个选择器。
SEL and @selector
编译后的选择器会分配给一个特定类型SEL,以区别其他数据。选择器永远不会是0.你必须让系统来为方法分配SEL标识符,重复分配是无效的。
@selector()指令让你可以直接引用编译后的选择符而不是方法名。 下面例子中,将选择符setWidth:height:分配给变量setWidthHeight:
SEL setWidthHeight; setWidthHeight = @selector(setWidth:height:);
在编译时使用@selector()指令将选择符分配给SEL变量是最有效率的。但是在某些情况下,你需要在运行时将一个字符串转化为一个选择器。你可以是用NSSelectorFromString方法来实现:
setWidthHeight = NSSelectorFromString(aBuffer);
相反还可以从选择器得到方法名。NSStringFromSelector方法从一个选择器返回一个方法名:
NSString *method; method = NSStringFromSelector(setWidthHeight);
方法和选择器
编译后的选择器标识的是方法名而不是方法本身的实现。例如,一个类有一个方法display,它和其他类中定义的方法display使用同一个选择器。这个是动态绑定和多态性的基础;这样可以使你可以向不同类型的接收者发送相同的消息。如果每一个方法都有一个选择器,那么消息机制就和方法调用完全相同了。
一个类方法和一个实例方法如果名字相同他们的选择器也相同。但是由于他们的作用域不同他们之间并不会造成混淆。一个类可以定义了一个display实例方法的同时再定义一个display类方法。
方法返回值类型和参数类型
消息只有通过选择器才能访问一个方法的实现,所以它对同名方法(拥有相同的选择器)的处理都相同。它会从选择器中得到方法的参数类型和返回值类型。因此,除非向一个静态类型的接收者发送消息,而动态绑定需要所有相同名字的方法的返回值类型和参数类型全部相同。(除了静态类型是因为编译器可以通过类型来知道方法的实现。)
但是同名的类方法和实例方法可以有不同的参数类型和返回值类型。
在运行时改变消息
方法performSelector:
, performSelector:withObject:
, 和 performSelector:withObject:withObject:是在NSObject协议中定义的一组方法,他们都接受SEL类型的变量。这三个方法都可以直接映射为消息功能。例如:
[friend performSelector:@selector(gossipAbout:) withObject:aNeighbor];
等同于:
[friend gossipAbout:aNeighbor];
这几个方法使得在运行时改变消息变为可能,同时还允许改变接收消息的对象。变量名可以用在消息表达式的两个部分:
id helper = getTheReceiver(); SEL request = getTheSelector(); [helper performSelector:request];
在这个例子中,接收者 (helper
) 是在运行时获得的(通过一个虚拟的方法 getTheReceiver
),同时需要调用的方法(request)也是在运行时确定的(同样也是由一个虚拟的方法getTheSelector获得)。
performSelector:以及它相关的方法返回一个id类型的对象。如果调用的方法返回的值是其他类型可以被转化为合适的类型。(但是不是所有的类型都能转化;需要方法返回的是一个指针或者和指针类型相符合的类型。)
目标-动作设计模式
在用户接口控制的处理中,AppKit很好的使用了在运行时改变接收者和消息的功能。
NSControl对象可以用于由图形设备为应用发送指令。在软件中,大部分仿真的操作设备例如:按钮,开关,把手,文本栏,刻度盘,菜单项等设备他们处在应用与用户之间,他们负责将诸如键盘、鼠标等硬件设备所发出的指令转换为应用特有的指令。例如,一个叫“Find”的按钮会将一个鼠标点击事件转换为一个检索指令发送给应用以开始一次检索。
AppKit定义了一个模板用于创建控制器同时也定义好了一些现成的控制器。例如,NSButtonCell类定义了一个对象,你可以把它分配给一个NSMatrix实例变量并为它初始化大小、标题、图片、字体、快捷键。当用户点击一个按钮(或使用快捷键),theNSButtonCell对象会发出一个消息指令高告诉应用做什么。要做到这些,NSButtonCell对象不仅仅要初始化图片,大小,标题,而且要初始化消息的接收者。因此,一个NSButtonCell实例可以初始化为一个动作(发送的消息中需要调用的方法)和一个目标(消息的接收者)。
[myButtonCell setAction:@selector(reapTheWind:)]; [myButtonCell setTarget:anObject];
当用户点击了相应的按钮,按钮使用NSObject协议中的方法performSelector:withObject:发送一个消息。所有的消息都有一个参数,控制设备的实例负责发送这个消息。
如果objc不允许消息变化,那么所有NSButtonCell对象只能发送相同的消息;所要调用的方法名都只能固定写在 NSButtonCell代码中。这样我们就不能像现在这样用一个简单的机制将用户动作转化为一个消息,这样按钮和其他控制器都需要限制消息的具体内容。这种被限制的消息会使得一个对象不能响应多个按钮。这样就需要为每个按钮创建一个响应的目标对象,或者目标对象需要判断是哪个按钮发送的消息并以此来决定要执行什么。并且每次当你调整了用户接口都必须修改响应这些动作的方法。如果没有了动态消息会造成许多objc所极力避免的不必要的麻烦。
避免消息发送错误
如果一个对象收到一个消息调用一个它未定义的方法那么会引起一个错误。这和调用一个不存在的方法有些相似。但是由于发送消息是在运行时进行的,所以这些错误经常只有到程序实际运行时才会暴露出来。
当消息的选择器是一个常量并且知道接收者的类型时这种错误相对容易避免。例如你自己写了一个程序,你当然能够确定接收者能够响应什么。如果接收者是静态类型,编译器会替你完成校验。
但是,如果消息选择器或者接收者的类型是可变的,这种校验就只能到运行时才能进行。NSObject类中定义的respondsToSelector:方法可以检验一个接收者是否可以响应一个消息。它把方法选择器作为参数并且返回接收者是否有一个与选择器相匹配的方法:
if ( [anObject respondsToSelector:@selector(setOrigin::)] ) [anObject setOrigin:0.0 :0.0]; else fprintf(stderr, "%s can’t be placed\n", [NSStringFromClass([anObject class]) UTF8String]);
当你向一个对象发送消息但是这个对象在编译时不能完全控制时 respondsToSelector:校验就显得特别重要。例如,你有一个代码会根据一个变量向一个对象发送消息来运行不同的方法,那么你就需要确保接收者实现了需要运行的方法。
欢迎交流讨论。转载请注明出处。本文地址: http://www.sjslibrary.com/?p=263