第04章
对象的类型和动态绑定
Objective-C的一个重要特征就是动态性,本章将对Objective-C的动态类型(dynamic
typing)和动态绑定(dynamic binding)进行说明。
4.1 动态绑定
4.1.1 什么是动态绑定
Objective-C中的消息是在运行时才去绑定的。运行时系统首先会确定接受者的类型(动态类型识别),然后根据消息名在类的方法列表里选择相应的方法执行,如果没有找到就到父类中去继续寻找,假如一直找到NSObject也没有找到就要调用的方法,就会报告不能识别消息的错误。
动态绑定(dynamic binding)指的就是在程序执行时才能确定对象的属性和需要相应的消息。
4.1.2 多态
在面向对象的程序设计理论中,多态(polymorphism)是指,同一操作作用不不同的类的实例时,将产生不同的执行结果。即不同的类的对象收到相同的消息时,也能得到不同的结果。
polymorphic [?p?l?‘m?:f?k] adj.
多形的,多态的,多形态的;
多晶形
polymorphism [?p?l?‘m?:f?z?m] n.
多型现象,多态性; 多机组合形式;
多形性
prism |?pr?z?m| noun
棱镜
多态是面向对象编程的一个重要特征,大大增强了软件的灵活性和扩展性。
例如若使用面向过程的思想,则每增加一种处理就有可能需要增加一个分支,而且可能需要在所有相关的地方增加处理。但是使用多态,就可以大大减少类似情况,可能只需要增加一个类即可。
4.2 作为类型的类
4.2.1 把类作为一种类型
我们可以把定义好的类
作为
对象的类型。如NSObject
*object;
4.2.2 空指针nil
如果给nil变量发送消息会怎么样呢?虽然这种写法是完全有效的,但是运行时不会有任何作用,消息也不会被发送。
如下例子:
if((val = [list entryForKey:”NeXT]) != nil)
{
[val increment]; //increment |???kr?m?nt| noun
增加、增值
}
给nil变量发送消息,消息也不会被发送,所以上面的程序就等价于下面的程序。
val = [list entryForKey:”NeXT”];
[val increment];
第二种写法虽然使程序变得更简洁,但却使程序变的更难理解。因为程序的本意是返回值为nil时不做任何处理。另外,不注意的话这种写法也会带来各种各样的错误,例如下面这段程序。
if ((val = [list entryForKey:”NeXT”]) != nil)
{
[val setValue:n++];
}
虽然val是nil是消息不会被发送,但是并不意味着[val
setValue:n++]不会被执行。如果上面这段代码中省略了val != nil的判断,n++就仍然会被执行,这点同判断语句中的“短路”有所不同。
那么,如果向nil发送消息,那么这个消息的返回值是什么?一般来说,如果消息对应的方法的返回值是一个对象,那么将返回nil;如果消息对应的方法的返回值是指针类型,将返回NULL;如果消息对应方法的返回值是整数类型,则将返回0。而如果返回值的类型是以上这几种类型以外的类型,例如结构体或者实数,那么返回值将是未定意的;
typedef struct
{
int one;
int two;
} MyTestStruct;
- (MyTestStruct)structTestFunc
{
MyTestStruct result = {1,2};
return result;
}
test = nil;
MyTestStruct myStruct = [test
structTestFunc];
NSLog(@"%d,%d,%lu",myStruct.one,myStruct.two,sizeof(myStruct));//0,0,8
NSLog(@"%d,%d,%lu",[test
structTestFunc].one,[test
structTestFunc].two,sizeof([test
structTestFunc]));//0,0,8
4.2.3 静态类型检查
虽然在Objective-C中id数据类型可以用来存储任何类型的对象,但绝大多数情况下我们还是将一个变量声明为特定类的对象,这种情况称为静态类型。使用静态类型时,编译器在编译时可以进行类型检查,如果类型不符合会提示警告。
在Objective-C中,id类型是一种通用的数据类型,类似于C语言的(void*),可以用来存储任何类的对象。在程序执行期间,这种数据类型真正的优势就会体现出来,使用id类型结合多态,可以使程序具备更大的灵活性和可伸缩性。但程序变灵活的同时,也需要付出代价,编译器不会对id类型的变量进行检查,这会导致程序更容易出错。
在一些复杂的程序中,使用强制类型转换时,也一定要注意类型,否则就会引发运行时错误。
对于NSObject对象,即使进行强制类型转换,实际执行时的方法也是有对象本应有的类型决定。
4.2.4 静态类型检查的总结
(6)若要判断到底是哪个类的方法被执行了,不要看变量所声明的类型,而要看实际执行时这个变量的类型。
(7)id类型并不是(NSObject*)类型,id类型和其他类之间没有继承关系。
C++和Java的多态是基于类层次结构的,可以通过子类重写父类中的方法来实现多态。这种类型的语言无法实现那种没有继承关系的多态,而OC中结合id类型可以做到这一点。
4.3 编程中的类型定义
4.3.1 签名不一致时的情况
消息选择器(message selector)中并不包含参数和返回值的类型的信息,消息选择器和这些类型信息结合起来构成签名(signature),签名被用于在运行时标记一个方法。接口文件中方法的定义也叫做签名。
NSArray * arry =
@[@1,@2];
[self
signature:(NSString *)arry
number:2];
- (void)signature:(NSString*)str number:(NSInteger)num
{
NSLog(@"%@,%p,%u",str,str,num);
/*
2016-06-01 17:12:53.207 Test[1153:80130] (
1,
2
),0x7d276fa0,2
*/
}
Cocoa提供了类NSMethodSignature,以面向对象的方式来记录方法的参数个数、参数类型和返回值类型等信息。这个类的实例也叫做方法签名。
重载(over loading)指的就是一个函数、运算符或方法定义有多种功能,并根据情况来选择合适的功能。
OC是动态语言,参数的类型是在运行时确定的,所以不支持这种根据参数类型的不同来调用不同函数的重载。OC可以通过动态绑定让同一个消息选择器执行不同的功能来实现重载。
4.3.2 类的前置声明
通过编译指令@class告知编译器某个符号是一个类名。这种写法称为类的前置声明(forward declaration)
class指令后面可以一次接很多个类,用“,”号分割,用“;”表示结束。前置声明也可以声明多次。如:
@class NSString,NSArry,NSMutableArry;
@class Volume;
通过使用@class可以提升程序整体的编译速度。但是要注意的是,如果新定义的类中要使用原有类的具体成员或方法,就一定要引入原有类的头文件。
@class的另外一个好处是,当多个接口出现类的嵌套定义时,如果只是相互包含对方的头文件无法解决,则只能通过类的前置声明来解决。
4.3.3 强制类型转换的使用示例
虽然强制类型转换的功能很强大,但会让编译器的类型检查变得没有意义,所以要尽量少用。不得不使用的情况下,要重新思考设计是否合理。
4.4 实例变量的数据封装
4.4.1 实例变量的访问权限
ClassA.h
//
// ClassA.h
// Test
//
// Created by ranzhou on 16/6/1.
// Copyright ? 2016年 ranzhouee. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface ClassA : NSObject
{
NSString *A_default;
//默认为protected
@public
NSString *A_public;
@protected
NSString *A_protected;
@private
NSString *A_private;
}
-(void)accessTestFun_A;
@end
@interface ClassA_Inline :
ClassA
-(void)accessTestFun_A_Inline;
@end
ClassA.m
//
// ClassA.m
// Test
//
// Created by ranzhou on 16/6/1.
// Copyright ? 2016年 ranzhouee. All rights reserved.
//
#import "ClassA.h"
@interface ClassA()
//在匿名类别中也能声明实例变量
{
NSString *A_category_default;//默认为private
@public
NSString *A_category_public;
@protected
NSString *A_category_protected;
@private
NSString *A_category_private;
}
@end
@implementation ClassA
-(void)accessTestFun_A
{
A_default = @"A_default";
A_public = @"A_public";
A_protected = @"A_protected";
A_private = @"A_private";
NSLog(@"%@\t%@\t%@\t%@",A_default,A_public,A_protected,A_private);
A_category_default = @"A_category_default";
A_category_public = @"A_category_public";
A_category_protected =
@"A_category_protected";
A_category_private =
@"A_category_private";
NSLog(@"%@\t%@\t%@\t%@",A_category_default,A_category_public,A_category_protected,A_category_private);
}
@end
@implementation ClassA_Inline
-(void)accessTestFun_A_Inline
{
A_default =
@"A_Inline_default";
A_public =
@"A_Inline_public";
A_protected =
@"A_Inline_protected";
//A_private = @"A_Inline_private";//Instance varible ‘A_private‘ is private.
NSLog(@"%@\t%@\t%@",A_default,A_public,A_protected);
//A_category_default = @"A_Inline_category_default";//Instance varible ‘A_category_default‘ is private.
A_category_public =
@"A_Inline_category_public";
A_category_protected =
@"A_Inline_category_protected";
//A_category_private = @"A_Inline_category_private";//Instance varible ‘A_category_private‘ is private.
NSLog(@"%@\t%@",A_category_public,A_category_protected);
}
@end
ClassB.h
//
// ClassB.h
// Test
//
// Created by ranzhou on 16/6/1.
// Copyright ? 2016年 ranzhouee. All rights reserved.
//
#import "ClassA.h"
@interface ClassB : ClassA
-(void)accessTestFun_B;
@end
ClassB.m
//
// ClassB.m
// Test
//
// Created by ranzhou on 16/6/1.
// Copyright ? 2016年 ranzhouee. All rights reserved.
//
#import "ClassB.h"
@implementation ClassB
-(void)accessTestFun_B
{
A_default =
@"A_default_In_B";
A_public =
@"A_public_In_B";
A_protected =
@"A_protected_In_B";
//A_private = @"A_private_In_B";//Instance varible ‘A_private‘ is private.
NSLog(@"%@\t%@\t%@",A_default,A_public,A_protected);
//A_category_public = @"A_category_public_In_B";//Use of undefintifier ‘A_category_public‘.
}
@end
ViewController.m中的测试代码
//----------------------------------------------------------------------------------
ClassA *testA = [[ClassA
alloc]init];
//testA->A_default = @"A_default";//Instance varible ‘A_default‘ is protected.
//testA->A_protected = @"A_protected";//Instance varible ‘A_protected‘ is protected.
//testA->A_private = @"A_private";//Instance varible ‘A_private‘ is private.
testA->A_public =
@"A_public";
//testA->A_category_default = @"A_category_default";//ClassA does not have a member named‘A_category_default‘;
[testA accessTestFun_A];
/*
2016-06-01 19:17:15.446 Test[1424:104376] A_default
A_public A_protected
A_private
2016-06-01 19:17:19.668 Test[1424:104376] A_category_default
A_category_public A_category_protected
A_category_private
*/
//----------------------------------------------------------------------------------
ClassB *testB = [[ClassB
alloc]init];
testB->A_public =
@"A_public_In_B";//同上仅仅有此实例变量可以访问。
[testB accessTestFun_A];
/*
2016-06-01 19:17:42.794 Test[1424:104376] A_default
A_public A_protected
A_private
2016-06-01 19:17:45.114 Test[1424:104376] A_category_default
A_category_public A_category_protected
A_category_private
*/
[testB accessTestFun_B];
/*
2016-06-01 19:17:57.460 Test[1424:104376] A_default_In_B
A_public_In_B A_protected_In_B
*/
//----------------------------------------------------------------------------------
ClassA_Inline *testA_Inline = [[ClassA_Inline
alloc]init];
testA_Inline->A_public =
@"A_public_Inline";//同上仅仅有此实例变量可以访问。
[testA_Inline accessTestFun_A_Inline];
/*
2016-06-01 19:25:17.490 Test[1519:108504] A_Inline_default
A_Inline_public A_Inline_protected
2016-06-01 19:25:19.001 Test[1519:108504] A_Inline_category_public
A_Inline_category_protected
*/
//----------------------------------------------------------------------------------
4.4.2 访问器
就是getter与setter方法。
一切都是为了封装。类中包含哪些实例变量以及怎么使用这些实例变量都和类的实现密切相关。如果是直接访问类的实例变量,那么当类的实现发生变化时就要做大量修改,如果是通过getter/setter接口,那么只需要修改接口即可。
4.5 类对象
4.5.1 什么是类对象
面向对象的语言中对类有两种认识,一种认为类知识作为类型的定义,程序运行时不作为实体存在;另外一种认为类本省也作为一个对象存在。我们把后一种定义中类的对象叫做类对象(class
object)。这种情况下类定义分为两个部分,一部分定义所生成的实例的类型,另外一部分则定义类自身的行为。OC把对作为对象看待,而在C++中,类只是作为类型定义使用。
类对象有自己的方法和变量,分别称为类方法和类变量。OC中只有类方法的概念,没有类变量的概念。另外,OC中类对象也称为factory,类方法也称为factory
method。
类方法中一个典型操作就是创建类的实例对象。类对象收到alloc这种消息后会生成类的实例。通过向类发送消息可以生成实例对象,那么类对象自身是什么时候生成的呢?类对象是程序执行时自动生成的,每个类只有一个类对象,不需要手动生成。
4.5.2 类对象的类型
假设把类对象也作为对象来处理,那么可以为类对象进行赋值操作吗?答案是肯定的。
id类型可以表示任何对象,类对象也可以用id类型来表示。Objective-C中还专门定义了一个Class类型用来表示类对象,所有的类对象都是Class类型。Class和id一样都是指针类型,只是一个地址,并不需要了解实际指向的内容。Nil被用来表示Class类型的空指针,实际的值是0.
//typedef struct objc_class *Class;
Class theClass = test ? [NSString
class]:[NSArray
class];
id testClass = [[theClass
alloc]init];
将类名定义为消息接收者是类对象特有的功能,除此之外类名只能在类型定义时使用。
类NSObject有一个class实例方法。所有的实例对象都可以使用class实例方法,这个方法返回的是对象所属的类对象。
4.5.3 类方法的定义
类方法的名字可以和实例方法甚至和实例变量的名字一样。
类方法中不能访问类中定义的实例变量和实例方法。类对象只有一个,类的实例对象可以有任意个。所以,如果类对象可以防问实例变量,就会不清楚访问的到底是哪个实例对象的变量。类方法中也不能访问实例方法。
其次,类方法在执行时用self代表了类对象自身,因此可以通过给self发送消息的方式来调用类中的其他类方法。同实例方法一样,也要注意self实际指向的类对象。
调用父类的类方法是,可以使用super。
4.5.4 类变量
OC中不支持类变量,但通过静态变量(static variables)可以模仿类变量。
4.5.6 类对象的初始化
由于类对象在程序执行的时候就已经生成了,因此不能通过发送消息的的方法来进行初始化。
OC的根类NSObject中存在一个initialize类方法,可以通过这个方法来为各类对象进行初始化。在每个类接收到消息之前,为这个类调用一次initialize,调用之前要先调用父类的initialize方法。每个类的initialize方法只能被调用一次。
因为在初始化的过程中会自动调用父类的initialize方法,所以子类的initialize方法中不用显示调用父类的initialize方法。
如果一个类中没有实现initialize方法,其父类的initialize方法就会被调用两次,面向自己一次,面向子类一次。所以实现initialize方法时要能确保该方法可以被重复调用。以免其子类没有实现initialize方法。
4.5.6 初始化方法的返回值
初始化方法的返回值一般都应该是id类型,起始写成类本身的类型也没啥么问题。只不过容易引起误解。
如下代码:
#import <Foundation/Foundation.h>
@interface ClassA :
NSObject
- (ClassA*)init;
@end
#import "ClassA.h"
@implementation ClassA
- (ClassA*)init
{
self = [super
init];
NSLog(@"%@",NSStringFromClass([self
class]));
return
self;
}
@end
#import "ClassA.h"
@interface ClassB :
ClassA
-(ClassB*)init;
@end
#import "ClassB.h"
@implementation ClassB
-(ClassB*)init
{
self = [super
init];
NSLog(@"%@",NSStringFromClass([self
class]));
return
self;
}
@end
ViewController 中有测试代码如下:
ClassA *testA = [[ClassA
alloc]init];
/*
2016-06-01 22:24:39.087 Test[2278:197930] ClassA
*/
ClassB *testB = [[ClassB
alloc]init];
/*
2016-06-01 22:24:43.651 Test[2278:197930] ClassB
2016-06-01 22:25:12.178 Test[2278:197930] ClassB
*/
再看一个增加代码灵活性的例子:Volume类中有:
[[Volume alloc] initWithMin:a max:b step:s];
比起该写法,一种更好的写法是
[[[self class]alloc] initWithMin:a max:b step:s];
新写法中使用[self class]代替了具体的类名Volume。class是类方法,返回self所属的类对象。这样修改后,子类可以原封不动地使用这行代码。volume
|?v?lju?m, American -j?m| n 容积、音量