动态绑定

什么是动态绑定

实际的程序会使用各种各样的类的实例对象,所有这些对象都可以用id类型来表示,因为id是通用的对象类型,可以用来存储任何类的对象。但这样一来,程序中就会出现无法区分某个实例对象到底是哪个类的对象的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import <Foundation/NSObject.h>
#import <stdio.h>

@interface A : NSObject
- (void)whoAreYou;
@end

@implementation A
- (void)whoAreYou { printf("I'm A\n"); }
@end

@interface B : NSObject
- (void)whoAreYou;
@end

@implementation B
- (void)whoAreYou { printf("I'n B\n"); }
@end

int main(void) {
id obj;
int n;

scanf("%d", &n);
switch (n) {
case 0: obj = [[A alloc] init]; break;
case 1: obj = [[B alloc] init]; break;
case 2: obj = [[NSObject alloc] init]; break;
}
[obj whoAreYou];
return 0;
}

该程序会根据从终端读入的数字的不同,让obj指向不同的类的对象。类A和B中都实现了whoAreYou方法,NSObject中没实现这个方法。但编译时不会给出任何警告信息,会正常生成可执行文件。但是执行程序时一旦输入2,则会获得以下异常

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '- [NSObject whoAreYou]: unrecognized selector sent to instance 0x103f00'

出错的原因在于类NSObject的实例对象中没有实现whoAreYou方法,所以出现运行时错误。而编译时之所以没出错,是因为编译时无法确定存储在id中的对象的类型。
Objective-C中的消息是在运行时才去绑定的。运行时系统首先会确定接收者的类型(动态类型识别),然后根据消息名在类的方法列表里选择相应的方法执行,如果没有找到就到父类中继续寻找,假如一直找到NSObject也没有找到要调用的方法,就会报告上述不能识别消息的错误。
动态绑定(dynamic binding) 指的就是在程序执行时才确定对象的属性和需要响应的消息。

多态

在面向对象的程序设计理论中,多态(polymor phism) 是指,同一操作作用于不同的类的实例时,将产生不同的执行结果。即不同类的对象收到相同的消息时,也能得到不同的结果。
我们通过一个绘制图形的例子来理解多态的特性。在面向过程的程序设计中,应该如此编写:

1
2
3
4
5
6
7
8
9
10
11
switch(target->kind) {
case Line:
lineDragged(direction);
break;
case Circle:
circleMove(direction);
break;
case Rectangle:
RectangleMove(direction);
break;
}

而使用多态的情况下,只要将各个图形都定义成一个类,其中实现自己的direction方法,并根据target实际指向的对象的不同,来调用不同的direction操作

1
[target move:direction];

多态的另外一个优点就是:利用继承可以更容易的定义新的类

作为类型的类

把类作为一种类型

我们可以把定义好的类作为对象的类型,也可以作为变量的类型,方法或函数的参数和返回值类型。

1
2
3
4
5
Volume *v1, *v2;
v1 = [[Volume alloc] initWithMin: 0 max: 10 step1: 1];
v2 = v1;
[v1 up];
printf("%d\n", [v2 value]);

其中v2指向v1所指的对象。所以输出的是改变后的值。同样,当消息的参数是对象时,实际上传递的是指向这个对象的指针而不是对象本身,根据消息处理中操作的不同,有可能会更改对象的值。

空指针nil

Objective-C中,nil表示一个空的对象,这个对象的指针指向空。nil是指向id类型的指针,值为0 。初始化方法失败的时候通常会返回nil。
新生成一个实例变量的时候,alloc方法会把数值类型的实例变量初始化为0,id和其他类型的指针变量也会被初始化为nil
返回值为id类型的方法中,如果处理出错的话一般也会返回nil。调用端会采用如下语句来判断方法调用是否成功

1
if ([list entryForKey: "NeXT"])

静态类型检查

虽然在Objective-C中id数据类型可以用来存储任何类型的对象,但绝大多数情况下我们还是将一个变量声明为特定类的对象,这种情况称为静态类型。使用静态类型时,编译器在编译时可以进行类型检查,如果类型不符合会提示警告。
我们可以通过下面的这个例子来看看Objective-C的类型检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#import <Foundation/NSObject.h>
#import <stdio.h>

@interface A : NSObject
- (void)whoAreYou;
@end

@implementation A : NSObject
- (void)whoAreYou { printf("I'm A\n"); }
@end

@interface B : A
- (void)whoAreYou;
- (void)sayHello;
@end

@implementation B
- (void)whoAreYou { printf("I'm B\n"); } /* override */
- (void)sayHello { printf("Hello\n"); }
@end

@interface C : NSObject
- (void) printName;
@end

@implementation C
- (void)printName { printf("I'm C\n"); }
@end

int main(void) {
A *a, *b;
C *c;
a = [[A alloc] init];
b = [[B alloc] init];
c = [[C alloc] init];
[a whoAreYou];
[b whoAreYou];
[c whoAreYou];
return 0;
}

类A和类是C是相互独立的两个类,类B是类A的子类。main函数中所有的变量都采用静态类型定义
编写这段程序会有如下警告。

1
warning: 'C' may not respond to 'whoAreYou'

虽然变量a和b都被声明为类A类型的变量,但编译器并没有提示警告。原因在于,如果仅仅使用父类中定义的功能,则变量的类型声明为父类也是没有问题的。
如果我们修改b调用的方法为

1
[b sayHello];

则会提示报错,因为我们将b定义为类型A。不过程序可以执行,因为b确实有sayHello方法。
我们可以使用强制类型转换,不让编译起报错

1
[(B *)b sayHello];

但是

1
[(B *)a whoAreYou];

实际输出还是I'm A,也就是说强制类型转换无效,实际执行的方法是由对象的类型决定的。

静态类型检查的总结

  1. 对于id类型的变量,调用任何方法都能够通过编译(当然调用不恰当的方法会出现运行时错误)
  2. id类型的变量和被定义为特定类的变量之间是可以相互赋值的
  3. 被定义为特定类对象的变量(静态类型),如果调用类或父类中未定义的方法,编译器就会提示警告
  4. 如果是静态类型的变量,子类类型的实例变量可以赋值给父类类型的实例变量
  5. 若是静态类型的变量,父类类型的实例变量不可以赋值给子类类型的实例变量
  6. 若是要判断到底是哪个类的方法被执行,不要看变量声明的类型,而是要看实际执行时这个变量的类型
  7. id类型并不是NSObject *类型
    Objective-C的静态类型检查是在编译期间完成的。向一个静态类型的对象发送消息时,编译器可以确保接收者可以响应该消息,否则会发出警告;当把一个静态类型的对象赋值给一个静态类型的变量时,编译器可以确保这种赋值是兼容的,否则会发出警告。运行时实际被执行的方法同变量定义是的类型无关。

编程中的类型定义

签名不一致时的情况

消息选择器中并不包含参数和返回值的类型的信息,消息选择器和这些类型信息结合起来构成签名( signature),签名被用于在运行时标记一个方法。接口文件中方法的定义也叫做签名

1
- (id)cellAtRow: (int)row column: (int)col;

Cocoa提供了类NSMethodSignature,以面向对象的方式来记录方法的参数个数、参数类型和返回值类型等信息。这个类的实例也叫做方法签名。
Objective-C中选择器相同的消息,参数和返回值的类型也应该是相同的。否则编译器就会提示警告
重载: Objective-C是动态语言,参数的类型是在运行时确定的,所以不支持这种根据参数类型的不同来调用不同函数的重载。但是可以通过动态绑定让同一个消息选择器执行不同的功能来实现重载

类的前置声明

当我定义一个类的时候,有时会将类实例变量、类方法的参数和返回值的类型指定为另外一个类。这种情况该如何定义: 可以在新定义的类的接口文件中引用原有类的头文件。
但是这种方式有一些缺点,头文件中除了类名之外,还有各种各样的其他信息的定义,而且还会引入新的引入。
如果仅仅是在类型定义的时候使用一下类名,则可以采用类的前置声明

1
2
3
#import <Foundation/NSObject.h>

@class Volume; // 声明要使用类Volume

通过编译指令@class 告诉编译器Volume是一个类名。使用@class可以提升程序整体的编译速度。但是如果新定义的类中要使用原有类的具体成员或方法,就一定要引入原有类的头文件。

实例变量的数据封装

实例变量的访问权限

Objective-C原则上不允许从对象外直接访问对象的实例变量。但是类A的方法中可以直接访问类A中包含self以外的其他实例的实例变量。同类型检查一样,能不能访问对象的实例变量也需要检查,这个检查在编译时完成。因此,只能访问使用静态类型定义的实例变量的内部变量。
下面是访问obj对象中实例变量myvar的语句写法

1
obj->myvar

只有被访问的类的实例对象与方法所在类的类型一致时才能访问。如果是其他类,即使是父类对象,则无法访问。

访问器

Objective-C不允许从外部直接访问和修改实例对象的属性。而仅仅可以访问同一个类的其他实例对象的变量。我们通常会定义专门的方法来访问或修改实例变量。
例如,类中有一个float类型、变量名交weight的属性,从类外部访问这个属性的方法应和属性同名,如下所示:

1
- (float) weight

定义修改改属性的方法时,可以用set作为前缀,之后接要更改的属性的名称,属性名的第一歌字母要求大写

1
- (void) setWeight: (float)value

这种用于读取、修改实例对象属性的方法称为访问器或访问方法。

实例变量的可见性

能否从外部访问实例变量决定了访问的**可见性(visibility)**。Objective-C中有四种可见性修饰符。但是需要注意的可见性修饰符并不影响通过访问器访问实例变量,只影响直接访问

  • @private: 只能在声明它的类内部访问,子类中不可以访问。
  • @protected: 能够被声明它的类和任何子类访问。没有显示置顶可见性的实例变量都是次属性
  • @public:作用范围最大,本类和其他类都可以直接访问
  • @package: 类所在的框架内,可以像public一样访问,框架外则同private
    可以在实现部分定义实例变量。因为实例变量定义在实现文件中,因此即便外部模块拿到接口文件,也不知道类中定义了那些实例变量。所以这种形式定义的实例变量,可见性默认是private的,子类无法直接访问,只能通过访问方法或者属性声明的方法访问

类对象

什么是类对象

面向对象的语言中对类有两种认识,一种认为类只作为类型的定义,程序运行时不作为实体存在。另外一种认为类本身也作为一个对象存在。我们把后一种定义中类的对象称为类对象
类对象有自己的方法和变量,分别称为类方法和类变量。与类实例的实例方法和实例变量进行区分。
Objective-C中类对象也称为factory,所以类方法也会被称为factory method.
类方法的一个典型例子就是创建类的实例对象: [Class alloc]。类对象接收alloc消息,并返回类的实例。
类对象在程序执行时自动生成,每个类只有一个类对象,不需要手动生成。每个类的所有实例对象都可以使用类方法。类方法可以访问类对象管理的变量。

类对象的类型

id类型可以表示任何对象,类对象也可以用id类型来表示。Objective-C中还专门定义了一个Class类型用来表示类对象。所有的类对象都是Class类型。Class和id一样都是指针类型,只是一个地址,并不需要了解实际指向的内容。Nil被用来表示空指针(是Class,而不是对象),实际值为0.
NSObject中定义了类方法class,所有的类都可以使用这个方法来获取类对象。

1
2
Class theClasss = flag ? [Volume class] : [MuteVolume class];
id v = [[theClass alloc] init];

将类名定义为消息接收者是类对象特有的功能,除此之外类名只能在类型定义时使用。
除了class类方法外,类NSObject还是有一个class实例方法。所有的实例对象都可以使用class实例方法,这个方法返回的是对象所属类的类对象。
类方法的定义方式: 与实例方法的定义基本相同,唯一的区别在于开头的-号换成+
继承的情况下,子类可以访问父类的类方法。
但是需要注意的是,类方法不能访问类中定义的实例变量和实例方法。类对象只有一个,类的实例对象可以有任意个。所以,如果类对象可以访问实例变量,就会不清楚访问的到底是哪个实例对象的变量。
其次,类方法在执行时使用self代表类对象自身,因此可以i通过给self发送消息的方式来调用类中的其他类方法。调用父类的类方法时,则使用关键字super

类变量

Objective-C不支持类变量(即静态成员变量)。Objective-C通过在实现文件中定义静态变量的方法来代替类变量。Objective-C在实现文件中定义了静态变量后,该变量的作用域就变为只在该文件中内有效。也就是说只有类的类方法和实例方法可以访问这个变量。在继承的情况下,因为static变量的作用域仅限于定义它的文件内,所以子类无法直接访问父类中定义的static变量,只能通过访问方法访问。

类对象的初始化

Objective-C的跟类NSObject中存在一个initialize类方法,可以通过使用这个方法来为各类对象进行初始化。在每个类接收到消息之前,为这个类调用一次initialize,调用之前要先调用父类的initialize方法。每个类的initialize方法只被调用一次。因为在初始化的过程中会自动调用父类的initialize方法,所以子类的initialize方法中不用显示调用父类的initialize方法。