此文章翻译自苹果官方文档原文地址:http://developer.apple.com/library/…TP30001163-CH11-SW10
消息
这部分讲解发送消息的语法,包括如何写一个消息表达式。同时讨论对象的实例变量的可见范围和多态性和动态绑定的概念。
消息语法
想要一个对象做点什么的时候,你给它发送一个消息告诉它去执行一个方法。在Objective-C中消息表达式要括在方括号中:
1 |
|
接收者是一个对象,消息告诉它要去做什么。在源码中消息只不过是发给接受者的一个方法名和一些变量。当消息发送的时候,运行时系统从接收者的方法列表中选择合适的方法并调用。
例如下面这个消息告诉myRectangle对象执行他的display方法来使矩形显示自己:
1 |
|
消息以“;”结尾和C语言的表达式一样。
因为方法名在消息中负责选择一个方法执行所以方法名在消息中通常被称为选择器。
方法可以传递参数,有一个参数的消息在方法名后面接一个“:”然后接一个参数:
1 |
|
对于有多个参数的方法方法名和参数交替出现这样方法名很自然的描述了所需的参数。下面例子这个消息告诉myRectangle 对象设置起始坐标为(30.0, 50.0):
1 2 3 |
|
选择器名包括方法名的所有部分,包括分号,所以上面例子中选择器名为setOriginX:y:。因为它有两个参数所以有两个分号。选择器不包含其他任何东西了例如返回值类型或参数类型。
重要:选择器名所包含的各部分不是可选的,他们的顺序也不能变化。在一些语言中“named parameters” 和 “keyword parameters”暗示着参数在运行时可变,可以有默认值,可以有不同的调用顺序,同时可能命名附加的参数。所有的这些参数的特性在 objective-c中都不可用。
其实一个Objective-C方法声明加两个额外的参数就是一个C语言函数声明。(C语言需要声明函数返回值类型和参数类型)(参考 “Messaging” in the Objective-C Runtime Programming Guide).因此Objective-C的方法声明结构和Python那种使用名字或关键字的声明结构是不同的。下面是一个Python的例子:
def func(a, b, NeatMode=SuperNeat, Thing=DefaultThing):
pass
在这个例子中,在方法调用时 Thing 和 NeatMode可以被省略或赋不同的值。
原则上一个Rectangle 类可以声明一个setOrigin::方法,第二个参数不标明标签,像如下这样调用
[myRectangle setOrigin:30.0 :50.0]; // 这是一个不好的多参数调用例子
尽管这个语法是合法的,setOrigin:: 没有交插显示方法名和参数。这样第二个参数实际上没有标明并且对于代码的阅读者很难判断这个方法的参数类型和用途。
方法的参数个数可变还是有可能的,尽管他们非常少见。附加的参数由逗号分隔接在方法名的后面。(和分号不同,逗号不是选择器名得一部分)在下面的例子中方法makeGroup:传递一个必须的参数(group)和三个可选的参数:
[receiver makeGroup:group, memberOne, memberTwo, memberThree];
和标准C函数一样,方法可以有返回值。下面的例子变量isFilled 当myRectangle是一个实心矩形时设为YES ,当它是一个空心矩形时设为NO 。
BOOL isFilled;
isFilled = [myRectangle isFilled];
注意:方法名和变量名可以相同。
消息表达式何以自身嵌套。如下一个矩形的颜色被设置到另一个矩形:
[myRectangle setPrimaryColor:[otherRect primaryColor]];
Objective-C也规定了点(.)操作符来提供一个紧凑方便的语法来调用对象的方法。点操作符一般和属性声明一起使用,参考 “Declared Properties” 关于他的介绍在 “Dot Syntax.”
向nil发送消息
在Objective-C中可以向nil发送一个消息只不过在运行时没有任何效果。在Cocoa中有几个范例都得益于此。向nil发送消息所返回的值也是有效的:
- 如果方法返回一个对象,那么向nil发送消息返回值仍为0(nil)。例如:
Person *motherInLaw = [[aPerson spouse] mother];
如果spouse 对象是空,那么mother 就是发送给nil并且返回nil。
- 如果方法返回指针类型,或是字节数不大于sizeof(void*)(在32位系统中长度为4字节)的整型integer,浮点型float,双 精度浮点型double,长双精度浮点型long double,长长整型long long那么发送到nil的消息返回值为0(数值)。
- 如果方法返回一个结构体,如果结构体是Mac OS X ABI Function Call Guide中定义的在寄存器中返回的,那么发送给nil的消息返回0,结构体中的每一个字段都为0。其他的结构体类型不会自动为0。
- 如果方法返回上述类型以外的类型,发送给nil的消息返回值为未定义(undefined)。
下面这段代码说明了如何使用向nil发送一个消息。
id anObjectMaybeNil = nil;
// this is valid
if ([anObjectMaybeNil methodThatReturnsADouble] == 0.0)
{
// implementation continues...
}
注意:向nil发送消息的处理在Mac OS X v10.5中做了一点变化。
在Mac OS X v10.4及以前版本,向nil发送消息也是有效的,只要消息返回一个对象,或是指针类型,或无返回值(void),或者小 于等于sizeof(void*)的整型,一个发送给nil的消息返回值也是nil。如果发送到nil的消息返回上述以外的类型(例如返回数据结构型,浮 点型,类模版(vector))那么返回值为未定义(undefined)。因此在Mac OS X v10.4及其以前版本不能依赖返回值为nil来判断返回值类型为对象,或是指针类型,或无返回值(void),或者小于等于sizeof(void*) 的整型。
接收者的实例变量
方法默认可以访问接收对象的实例变量。你不需要把实例变量作为参数传递给方法。例如上面例子中的primaryColor 方法没有参数,然而它可 以找到otherRect的 primary color并且返回它。每个方法可以访问接收者和他的实例变量而不需要将他们声明为参数。
这个协定可以简化Objective-C源代码。同时提供了一种关于对象和消息的面向对象思考方式。消息发送给接收者就好比给你家寄一封信一样。消息的参数从外部携带信息发送给接收者,但是他们不需要把接收者本身传递给自己。
方法默认仅仅可以访问接收者的实例变量。如果它需要一个存储在其他对象中的变量内容,它必须发送一个消息到那个对象去要求它显示那个变量的内容。先前所展示的primaryColor 和isFilled 方法就是为了这个目的。
以后讲解类定义时会更多讲解关于实例变量。
多态性
像先前的例子说明的那样,Objective-C中的消息和标准C语言中的函数调用在语法位置上是相同的。但是由于方法“属于”一个对象,所以消息和函数调用在工作方式上是不同的。
特别是一个对象只能被那些它定义的方法操作。它不会和其他对象定义的方法混淆(不会被其他对象的方法操作),即使另一个对象有一个同名的方法。因此 两个对象可以对同一个消息做出不同的响应。例如每个种类的对象接收一个display 消息都可以按照他们各自的方式显示他们自己。例如圆形和矩形对于一 个完全相同的跟踪光标指令所做的响应可以是不同的。
这种特性被称为多态性,在面向对象程序设计中具有重要作用。它和动态绑定一起允你所写的代码适用于许多不同种类 的对象并且在写代码的时候并不需要去确定这些对象的类型。这些对象甚至可以在以后才被开发,或是由其他程序员开发。如果你写代码时发送一个 display 消息到一个id类型的变量,任何有display 方法的对象都是一个潜在的接收者。
动态绑定
方法调用和发送消息关键的不同在于方法和它的参数是在编译时结合在一起,但是消息和接收对象直到程序运行并发送消息时才被结合。因此调用哪个方法来响应一个消息只有在运行时才能决定而不是在代码编译时。
当一个消息被发送,一个运行时的消息分发例行程序找到消息里的接收者和方法名,然后定位到接收者所实现的与方法名相匹配的方法并调用方法,并且传给方法一个接收者的实例变量指针。(更多的关于消息分发例行程序参考“Messaging” in Objective-C Runtime Programming Guide)
消息和方法的动态绑定和多态的紧密结合使面向对象编程更灵活更强大。因为每个对象都可以有自己的方法,Objective-C语句可以获得多种不同 的结果,而不需要发送多个消息而只需发给不同的接收对象。接收者可以在程序运行时才被确定,选择哪个接收者可以取决于诸如用户操作的一些因素。
在运行基于AppKit的代码时,用户会决定哪个对象接收菜单指令诸如剪切、复制、粘贴。信息被发送到当前选中 的对象。一个显示文字的对象和一个显示扫描图的对象对于copy 指令的消息会做出不同的响应。一个用于显示一类图形的对象和用于显示矩形的对象对于 copy 消息也会有不同的响应。因为消息直到运行时才会调用方法(换句话说直到运行时方法和消息才会绑定)所以这些方法的处理也是各自相互独立的。发送 消息的代码不需要关心接收者如何处理,甚至不需要考虑接收者能否对请求做出响应,应用程序所包含的各个对象可以按照自己的方法对copy 消息做出响应。
Objective-C对于动态绑定做出了更好的支持,它甚至允许在运行时使用一个变量作为方法名(选择器)来发送消息。这个机制在“Messaging” in Objective-C Runtime Programming Guide.中进行讨论。
动态方法解决方案
你可以在运行时使用动态方法解决方案对类和实例方法进行实现。参考“Dynamic Method Resolution” in Objective-C Runtime Programming Guide
点语法
Objective-C提供了一个点操作符来替代方括号([])进行方法调用。点语法使用和访问C语言结构体元素相同的格式:
myInstance.value = 10;
printf("myInstance value: %d", myInstance.value);
当我们使用对象时点语法使代码更简单易读,编译器会把它编译为访问方法(对于accessor method我并不确定这样翻译是不是正确应该是类似于其他语言中的get/set方法)。点语法不会直接读取或修改一个实例变量。上面的这段代码和下面这段是完全相同的:
[myInstance setValue:10];
printf("myInstance value: %d", [myInstance value]);
如果一个对象想要通过访问方法访问自己的实例变量那就必须明确的使用self:
self.age = 10;
等价于:
[self setAge:10];
如果不使用self.,那么就是直接访问的实例变量,像下面这样就没有调用age 的访问方法:
age = 10; |
点语法的一个好处就是它比方括号看上去更简介易读,特别是当你想要访问或修改另一个对象的属性时。另一个好处是编译器可以在代码试图修改一个只读属 性时报错。如果使用方括号语法访问变量,编译器在最好情况下只能报出一个你引用了一个不存在的setter方法的未声明方法警告,并且代码会在运行时运行 失败。
一般用法
当使用点语法取一个值的时候,系统会调用相关的getter访问方法。默认的getter方法名为点以后的字符。使用点语法给变量赋值会调用关联的 setter访问方法。默认的setter方法名将为点以后的字符首字母大写并在前面加一个set。如果你不希望使用默认的访问方法名,你可以使用声明对 象属性来修改他们。(参考“Declared Properties”).
Listing 1 几个使用方法的例子。
Listing 1-1 使用点语法访问属性
Graphic *graphic = [[Graphic alloc] init];
NSColor *color = graphic.color;
CGFloat xLoc = graphic.xLoc;
BOOL hidden = graphic.hidden;
int textCharacterLength = graphic.text.length;
if (graphic.textHidden != YES) {
graphic.text = @"Hello"; // @"Hello" is a constant NSString object.
}
graphic.bounds = NSMakeRect(10.0, 10.0, 20.0, 120.0);
Listing1-2和listing1-1在编译后完全相同,只不过是listing1-2使用了方括号语法。
Listing 1-2 使用方括号语法访问属性
Graphic *graphic = [[Graphic alloc] init];
NSColor *color = [graphic color];
CGFloat xLoc = [graphic xLoc];
BOOL hidden = [graphic hidden];
int textCharacterLength = [[graphic text] length];
if ([graphic isTextHidden] != YES) {
[graphic setText:@"Hello"];
}
[graphic setBounds:NSMakeRect(10.0, 10.0, 20.0, 120.0)];
对于适当的C语言类型的属性,符合赋值符的含义显得更清晰明了。例如有一个NSMutableData 类的实例:
NSMutableData *data = [NSMutableData dataWithLength:1024];
你可以使用点语法和符合赋值符来更新实例的length属性:
data.length += 1024;
data.length *= 2;
data.length /= 4;
等价于下面这段使用方括号的代码:
[data setLength:[data length] + 1024];
[data setLength:[data length] * 2];
[data setLength:[data length] / 4];
nil值
如果在属性遍历过程中遇到一个nil值那么所得到的结果和向nil发送一个等价的消息是相同的。例如,下面的两对代码是相等价的:
// Each member of the path is an object.
x = person.address.street.name;
x = [[[person address] street] name];
// The path contains a C struct.
// This will crash if window is nil or -contentView returns nil.
y = window.contentView.bounds.origin.y;
y = [[window contentView] bounds].origin.y;
// An example of using a setter.
person.address.street.name = @"Oxford Road";
[[[person address] street] setName: @"Oxford Road"];
性能和线程处理
无论使用点语法还是方括号语法引用访问方法,编译器生成的代码的是相等的。因此这两种编码技巧所获得的结果与性能是完全相同的。由于使用点语法仅仅是引用访问方法的一个途径而已,所以也不会 引入额外的线程开销。
点语法的用法
作为方括号语法的一种替代,使用点语法引用访问方法:
- 下面这句代码引用aProperty 的getter方法并将返回值赋给变量aVariable:
aVariable = anObject.aProperty;
- 属性aProperty 的类型和变量aVariable 必须是相兼容的,否则编译器会抛出一个警告。
- 下面这句引用了对象anObject 的setName: 方法并将@”New Name”作为参数传递给方法。
anObject.name = @"New Name";
- 如果setName: 方法不存在,或者属性name 不存在,或者setName: 方法返回一个void以外的类型编译器都会报一个警告。
- 下面这句代码引用了对象aView 的方法bounds 。bounds 方法返回一个NSRect 类型的对象,然后将返回值中一个结构体的元素的值origin.x赋给变量 xOrigin 。
xOrigin = aView.bounds.origin.x;
- 下面这句代码把11分别赋给两个属性:一个是对象anObject的integerProperty 属性,一个是anotherObject 的floatProperty 属性。
NSInteger i = 10;
anObject.integerProperty = anotherObject.floatProperty = ++i;
- 具体的说是从最右边开始进行计算(++i)然后把计算结果传递给setIntegerProperty: 和setFloatProperty:这两个setter方法。 计算结果的类型会在每个赋值时按照需要的类型进行强制类型转换。
点语法的错误用法
下面的代码是强烈不推荐的,因为他们没有遵守点语法引用访问方法的设计初衷。
- 下面这个代码会产生一个编译警告(warning: value returned from property not used.警告:返回值没有被使用)。
anObject.retain;
- 下面这句会报一个警告:setFooIfYouCan: does not appear to be a setter method because it does not return (void).
- setFooIfYouCan: 不是一个setter方法因为他返回值不为空。
/* Method declaration. */
- (BOOL) setFooIfYouCan: (MyClass *)newFoo;
/* Code fragment. */
anObject.fooIfYouCan = myInstance;
- 下面这句引用lockFocusIfCanDraw 方法将返回值赋给flag。这句不会产生一个警告除非flag 的类型和返回值类型不匹配。 尽管如此这样使用还是强烈不推荐的。因为lockFocusIfCanDraw 不是一个访问方法,而点语法只推荐用于调用访问方法(getter和 setter)也就是访问对象的属性或实例变量。
flag = aView.lockFocusIfCanDraw;
- 下面这句因为readonlyProperty 属性被声明为只读访问,因此会报一个警告:向只读属性 赋值。
/* Property declaration. */
@property(readonly) NSInteger readonlyProperty;
/* Method declaration. */
- (void) setReadonlyProperty: (NSInteger)newValue;
/* Code fragment. */
self.readonlyProperty = 5;
- 尽管如此由于setter方法是显示调用的,这段代码还是会运行的。强烈不推荐这样做是因为仅仅通过给属性添加一个setter方法并不能说明属性的读写权限,必须要在属性声明语句中明确的设置属性的读写权限。