《ObjC.pdf》
在Objective-C中,选择器selector有两种含义。其一是当在源代码中向一个对象发送消息时,用它来引用一个方法的名字;其二是当源代码编译过后,用它来引用取代名字的唯一标识符。编译后的选择器类型为SEL。所有相同名字的方法都有相同的选择器。你可以使用选择器来调用对象的方法——这为在Cocoa中实现target-action设计模式打下了基础。
方法和选择器
为了提高效率,在编译后的代码中,方法选择器不会使用完全ASCII名字。编译器将每个方法的名字写入一张表,然后将该名字与该方法在运行时的唯一标识符配对。运行时系统确保每个标识符都是唯一的:不存在相同的两个选择器,同名的所有方法都有相同的选择器。
SEL和@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的类方法。
方法返回和参数类型
消息机制只能通过选择器来访问方法的实现,因此它对所有具有相同选择器的方法一视同仁。它会从选择器中找到方法的返回类型,以及它的参数数据类型。因此,除了发送给静态类型接收者的消息外,动态绑定要求所有同名方法的实现都要有相同的返回类型和相同的参数类型。(对这条规则来说,静态类型的接收者是一个例外,因为编译器能够从类的类型了解方法实现。)
尽管同名的类方法和实例方法有相同的选择器标识,但它们可以有不同的参数类型和返回类型。
在运行时改变消息
在NSObject中定义的performSelector:、performSelector:withObject:和performSelector:withObject:withObject:方法,使用SEL标识符作为它们的初始参数。这三个方法都直接映射到消息函数。例如:
[friend performSelector:@selector(gossipAbout:) withObject:aNeighbor];
等价于:
[friend gossipAbout:aNeighbor];
这些方法使得在运行时改变消息成为可能,正如能够改变接收该消息的对象一样。变量名可以在消息表达式任何一部分使用:
id helper = getTheReceiver();
SEL request = getTheSelector();
[helper performSelector:request];
在这个例子中,接收者(helper)在运行时被选择(通过虚拟的getTheReciever函数),接收者要求执行的方法(request)也在运行时确定(同样使用虚拟的getTheSelector函数)。
注意:performSelector:和它的伙伴方法返回类型为id的对象。如果执行后的方法返回一个不同的类型,它将被转换为适当的类型。(但是,这种转换并不适用于所有类型,该方法将返回一个指针或兼容指针的一个类型。)
Target-Action设计模式
在处理用户-接口控件方面,AppKit充分发挥了在运行时改变接收者和消息的能力。
NSControl对象是一个图形设备,可以用来向应用程序发送指令,。大多实现了现实世界中的控制装置,例如button、switch、knob、text field、dial、menu item等。在软件中,这些设备处于用户和和应用程序之间。它们解释来自硬件设备,如键盘和鼠标的事件,并将它们翻译成应用程序特定的指令。例如,名为“Find”的按钮将会把鼠标点击事件翻译成开始搜索的应用程序指令。
AppKit为创建控件设备定义了模板,并定义了一些自己的现成设备。例如,NSButtonCell类定义了一个对象,可以指派给一个NSMatrix实例,并初始化它的大小、名称、图片、字体和键盘快捷键。当用户点击按钮(或使用键盘快捷键)时,NSButtonCell对象发送消息,指示应用程序工作。为此,NSButtonCell对象不仅要初始化图像、大小和名称,还要确定消息要发往何方和发给谁。相应地,NSButtonCell实例可以为一个action消息(它将在自己发送的消息中使用的对象选择器)和一个target(接收该消息的对象)进行初始化。
[myButtonCell setAction:@selector(reapTheWind:)];
[myButtonCell setTarget:anObject];
当用户点击了相应的按钮,该按钮单元将使用NSObject协议方法performSelector:withObject:发送消息。所有action消息带有单独一个参数,既发送该消息的控件设备的id。
如果Objective-C不允许改变消息,所有的NSButtonCell对象将不得不发送相同的消息,方法的名字将在NSButtonCell源代码中写死。与简单的实现将用户action转换为action消息的机制不同,按钮单元和其他控件不得不限制消息的内容。受限的消息会使很多对象难以响应多于一个的按钮单元。要么每个按钮有一个target,要么target对象能发现消息来自于那个按钮,并做相应处理。每次在重新布局用户接口时,你也必须实现响应action消息的方法。动态消息的缺乏将会带来不必要的麻烦,但Objective-C很好地避免了这一点。
避免消息错误
如果一个对象接收了一条消息去执行不归它管的方法,就会产生错误结果。这和调用一个不存在的函数是同一类错误。但是,因为消息发生在运行时,错误只有在程序执行后才会出现。
当消息选择器是常数并且接收对象类已知时,处理这种错误相对容易。在写程序时,你可以确保接收者能够响应。如果接收者时静态类型,编译器将替你完成该测试。
但是,如果消息选择器或接收者是变化的,那么只能在运行时进行相关测试。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:运行时测试非常重要。例如,如果你写了一段代码向一个对象发送消息,而这个对象是一个他人可以设定值的变量,那么你就要确保接收者实现了响应该消息的方法。
注意:一个对象在收到不是自己负责直接响应的消息时可以转发该消息给其他对象。这种情况下,从调用者的角度来看,对象直接处理了消息,尽管该对象是通过转发消息给其他对象来处理的。