“终于回到了工作岗位,断线了这么久,也着实很无奈,不过还好,每一段经历都会使人成长,生活不易,砥砺前行吧!”
该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
第3章 对象和消息传递
第2章我们已经知道了如何创建类,现在要讨论的是如何使用这些类;
本章还会介绍如何创建对象、初始化对象以及调用对象中的方法;
OC的类是通过实例变量、属性和方法定义的;通过OOP的继承性,类还会拥有其父类的状态和行为(即方法和实例变量);在运行时,面向对象的程序通过创建对象并使用消息传递操作这些对象来执行其逻辑;
消息传递:一种调用对象和类中方法的OOP机制;
3.1 创建对象
对象的创建:通过分配内存和初始化,可以创建类实例(即对象);
类型相同的对象称为同类成员;在创建同类对象时,实际上会创建一系列实例变量和指向类方法的指针;
每个类都拥有其本身的命名空间;
在类定义内部分配的名称不应与类定义外部分配的名称冲突;
消息必须能够被接收对象识别;
Foundation框架提供了简化对象创建工作的功能;尤其是NSObject类中包含了创建实例的方法:
NSObject类的alloc方法会返回指定类的新实例;(这点十分重要!init只是初始化,alloc才是创建实例的放阿飞)
+(id)alloc;
id:是OC中一种数据类型,用于存储对任何OC对象的引用,且不区分对象所属的类;
对比id和A*(A是一个类):
id的修饰变量名,表示一个数据类型为id的变量;
A*则表示数据类型被设置为类名*(指向对象所属类的指针);
显示定义变量的类型会通过牺牲灵活性,获得静态类型检查;
但无论使用哪种方式,alloc都会返回接收类类型的对象;
接收类类型也称为关联返回类型;
这意味着:通过发送alloc消息获得的返回对象,将会拥有与接收类相同的数据类型;
[A alloc];//表示我们向A发送了一个alloc消息,A就是接受类;
这个过程系统会为该对象分配内存,它的实例变量也会被设置为相应的默认值;
3.1.1 初始化对象
alloc方法为对象分配了内存,实例变量的值也设置为默认;但是alloc方法即没有将该对象的实例变量初始化为适当的值,也没有为这个对象准备其他必需的对象和资源;
因此,我们还需要实现一个完成该初始化过程的方法;
NSObject类实现了一个init方法,该方法为对象的初始化提供了基础,你编写的类至少需要包含这个方法的内容,才能提供自定义的初始化行为;
-(id)init;
注意:这是一个实例方法;返回的id类型的对象是关联返回类型;
我们来看一段已经非常熟悉的代码:
-(id)init{
if(self = [super init]){
//初始化
_propEle = @"none";
}
return self;
}
[super init]:通过使用父类的init方法,实现调用对象的初始化工作;
因为是第一句,它也确保了对象的一系列初始化工作完全根对象的结构;
self参数:OC的特殊参数,所有方法都有一个隐含参数self,它不存在于方法定义中;
它是一个指向接收消息对象的指针;
赋值语句self=[super init];确保实现符合对象类层次结构的初始化;
根据命名惯例,系统会永远使初始化方法的名称以init开头;
因为为对象分类内存之后(创建对象的过程),就会初始化对象,因此你可以这样做:
A * a = [[A alloc] init];
3.1.2 延伸Elements工程
(注:核心代码将附在文末)
定义类C3Atom及其子类C3Hydrogen(参见相应的类结构);
1 重构C3Atom类:
重构:是指在不更改外部行为的情况下重新构建现有代码,旨在为软件提高性能、新增特性等;
@property (nonatomic , readonly) NSString * chemicalElement;
这是C3Atom类接口中定义的一个属性,该属性是使用自动补全功能定义的;
当编译器自动补全属性时,它会创建一个作用范围为@private的支持实例变量;
私有的实例变量无法由该类的子类直接访问,所以需要重构这个类;
方案:
可在实例变量声明中,将这个变量的作用范围设置为@protected;
为了将自动补全功能生成的属性方法和实例变量链接起来,应根据支持属性的实例变量命名惯例来命名这个实例变量;(如 使用下划线前缀的属性名命名实例变量)
重构C3Atom类中的接口代码,添加另一个只读属性和对应的实例变量,作为用以检索C3Atom对象原子符号的属性;
补充修改实例方法;
这样声明的作用范围为@protected支持属性的实例变量,可以使其子类直接访问(和设置)这些实例变量;
2 创建C3Hydrogen类的初始化方法:
创建一个新的初始化方法:使用输入的中子数初始化对象,并设置化学元素名和原子符号;(参见该类实现)
3 创建C3Hydrogen类的工厂方法:
工厂方法:是指用于创建和初始化的便捷方法;
他们是类方法,命名通常遵循惯例;
+(id)类名...;//类名指类的名称,小写字母开头;也就是类名前缀;
如:+(id)hydrogenWithNeutrons:(NSUInteger)neutrons;
下面我们测试一下这几个类:
C3Atom * atom1 = [C3Hydrogen hydrogenWithNeutrons:0];
NSLog(@"%@", [atom1 logInfo]);
id atom2 = [C3Hydrogen hydrogenWithNeutrons:1];
NSLog(@"%@", [atom2 logInfo]);
C3Hydrogen * atom3 = [C3Hydrogen hydrogenWithNeutrons:1];
NSLog(@"%@", [atom3 logInfo]);
C3HOtherEle * atom4 = [C3HOtherEle hydrogenWithNeutrons:0];
NSLog(@"%@", [atom4 logInfo]);
1)如果工厂方法的实现是按如下方式的:
+(id)hydrogenWithNeutrons:(NSUInteger)neutrons{
return [[C3Hydrogen alloc] initWithNeutrons:neutrons];
}
我们注意到,继承C3Hydrogen的C3HOtherEle的loginfo打印出来的是:
2017-11-07 09:52:14.307415+0800 精通Objective-C[61609:12783060] C3Hydrogen
2017-11-07 09:52:14.307611+0800 精通Objective-C[61609:12783060] C3Hydrogen
2017-11-07 09:52:14.307743+0800 精通Objective-C[61609:12783060] C3Hydrogen
2017-11-07 09:52:14.307855+0800 精通Objective-C[61609:12783060] C3Hydrogen
2)如果工厂方法是这样的:
+(id)hydrogenWithNeutrons:(NSUInteger)neutrons{
return [[[self class] alloc] initWithNeutrons:neutrons];
}
loginfo打印出来的是:
2017-11-07 09:55:38.373540+0800 精通Objective-C[61634:12786187] C3Hydrogen
2017-11-07 09:55:38.373810+0800 精通Objective-C[61634:12786187] C3Hydrogen
2017-11-07 09:55:38.373962+0800 精通Objective-C[61634:12786187] C3Hydrogen
2017-11-07 09:55:38.374109+0800 精通Objective-C[61634:12786187] C3HOtherEle
我们可以得出这样的结论:
使用[self class]可以用来获取当前类实例;(区别于类对象实例)
在使用这个表达式时(与直接设置类相比),如果该类被子类化且这个工厂方法被子类调用,那么返回实例的类型就会与子类相同;(正如上面测试的一样)
3.2 发送消息
消息发送:是OOP的一个基础概念,是用于调用对象中方法的机制;
接收器:接收消息的对象,它会在运行时决定调用其实例的哪个方法;
实例方法可以访问对象的实例变量和实例方法;
向对象发送消息(即调用对象中的方法):
[接收器 消息名称和参数]
消息名称和参数:该信息指明了方法的名称和参数值(如果有参数的话);
...V1:value1 V2:value2 ... Vn:valuen
冒号经方法签名关键字和相关的参数值分隔开;没参数的话就省略冒号;
举一个例子:
[orderObject addItem:burgerObject forPrice:3.50];
消息名为:addItem: forPrice:;
接收器为:orderObject;
参数:burgerObject 和 3.50;
OC还支持多态,即不同的接收器可以拥有同一个方法的不同实现代码;
接收器的类型会在运行时确定,因此,在回应同一条消息时,不同的接收器会做出不同的事情;
点表达式:这是OC提供的另一种语法,可以简化调用属性访问器方法的语法;
对象名.属性名
对象名.属性名 = 值
类方法:OC为类方法提供了语言级支持,类方法是指在类级别上定义的并且直接在类上调用的方法;
[类名 消息名称和参数]
类方法无法访问为类定义的实例变量和方法;它们通常用于创建新的类实例(工厂方法),或访问与类关联的共享信息;
通过将消息(行为请求)与接收器(回应请求的方法的所有者)分隔开,对象消息传递为OOP封装模式提供了直接的支持;
OC通过语言级的功能增加了对对象消息传递的支持,使你可以在运行时决定调用哪个方法,甚至还可以在运行时更改方法的实现代码;我们之后会详细介绍;
3.3 消息转发
OC的对象消息传递特性根据接收到的消息,找到并执行对象中的方法;(注意理解这句话:消息不是方法,但消息可以决定调用哪个方法)
静态类型:在代码中指定对象的类型,并在编译时静态绑定;
动态类型:不在代码中指定对象的类型,而是在运行时再解析;
不论是哪种设置对象的类型,在运行时接收的对象都会通过解释消息,确定需要调用哪个方法;
这种运行时方法调用解决方案,使动态地更改或扩展程序变得更加容易,但同时也具有一定的危险性;
这种解决方案允许程序向可能没有相应方法的对象发送消息;默认这种情况,程序会抛出异常;
为此OC提供了另一种选择;
消息转发机制:
当对象收到与其方法集不匹配的消息时,通过消息转发机制可以使对象执行用户预先定义处理过程;
使对象能在收到无法识别的消息时执行各种逻辑,如将消息发送给能做出响应的其他接收器,将所有无法识别的消息都发给同一接收器,或者默认吞下消息(既不执行也不抛异常);
3.3.1 转发选项
OC提供了两种消息转发选项:
1)快速转发:
NSObject类的子类通过重写NSObject类的方法
-(id)forwardingTargetForSelector:(SEL)aSelector,将该方法转发给其他对象,从而实现快速转发;
该技巧就像是将对象的实现代码与转发对象合并到了一起;这类似于类实现的多重继承行为;(稍后我们会实践一下)
2)标准(完整)转发:
NSObject类的子类通过重写NSObject类的方法
-(void)forwardInvocation:(NSInvocation *)anInvocation,实现标准转发;
该技巧使对象能够使用消息的全部内容(目标 方法名 参数);
如果你拥有一个定义了对象能够消化哪些消息的目标类,快速转发可以取得很好的效果;
如果没有这样的目标类或者想要执行其他处理过程,就应该使用完整转发;
接下来我们看下如何使用;
3.3.2 向Hydrogen类添加快速转发机制
方案:
我们扩展类使之具备快速转发特性,并定一个一个目标类,通过实现相应的方法处理转发的消息;
消息转发辅助类:
新建辅助类C3HydrogenHelper继承自NSObject;(详见具体类实现)
定义了一个实例方法,返回一个字符串指针;
接下来修改C3Hydrogen类,添加方法-(id)forwardingTargetForSelector:(SEL)aSelector及其实现代码;
为C3Hydrogen类添加了一个实例变量,并在初始化方法中进行初始化;这个实例变量是用于进行消息转发的目标对象;
实现-(id)forwardingTargetForSelector:(SEL)aSelector方法;
该方法会先检查消息是否能够由目标对象(_helper)处理,如果能处理则返回目标对象,否则返回nil;
因为C3HydrogenHelper有实例方法-(NSString *)factoid;,因此若C3Hydrogen对象接收到了调用该方法的消息,就会将该消息发送给C3HydrogenHelper对象_helper;
我们来测试下:
如果直接向C3Hydrogen对象发送factoid消息,显然是不行的,因为编译不过;
编译器需要了解程序中可能发送的所有消息(包括转发消息)的完整方法签名;
解决发放是声明未知方法:可以在类接口或分类中声明它们;
我们尝试使用分类的方式,新建分类C3Atom+Helper;(因为我们不需要相应的.m文件,选中可删除)
C3Hydrogen * atomH = [C3Hydrogen hydrogenWithNeutrons:2];
NSLog(@"%@", [atomH logInfo]);
NSString * factH = [atomH factoid];
NSLog(@"%@", factH);
log:
2017-11-07 15:35:18.027727+0800 精通Objective-C[63440:13028457] C3Hydrogen
2017-11-07 15:35:18.027842+0800 精通Objective-C[63440:13028457] The lightest element!
需要注意的是:只有接受的消息与方法集不匹配的时候,才会使用消息转发机制,也就是说[atomH massNumber];的调用并不会走C3Hydrogen类中重写的-(id)forwardingTargetForSelector:(SEL)aSelector方法;
在C3Hydrogen类中我们同样实现了标准转发,大家可以参看下;可以看到,两者同时有的时候使用的是快速转发;
3.4 小结
本章介绍了创建和初始化OC对象,以及向OC对象传递消息的概念和细节;
本章要点:
1)使用NSObject类的alloc方法可以为对象分配内存,将对象的实例变量初始化为0;
该方法会返回它接收类的实例,这也称为与结果关联的类型;
2)NSObject提供的init方法可以完成对象初始化的基础任务;你需要为自己的类重写该方法,才能为自己编写的类提供自定义的初始化行为;
3)在实现初始化方法时,应遵循对象完整的继承链,对对象执行一系列的初始化操作;
4)OC的消息传递功能支持OOP多态性,即不同的接收器调用同一方法可以取得不同的结果;
此外,还可以在运行时确定接收器的类型;(编译器只是做静态类型的检查)
5)当对象接收到与其方法不匹配的消息时,通过OC的消息转发功能可以使对象执行用户预定义的处理过程;
使用消息转发机制可以提供许多与OOP多继承关联的功能;
6)OC提供了两种消息转发机制:快速转发与标准(完整)转发;
这两种机制都是通过重写NSObject类相应的实例方法并实现所有必需的支持类实现的;
最后附上本章使用的核心代码:
调试方法文件中需要导入的头文件:
#import "C3Hydrogen.h"
#import "C3HOtherEle.h"
#import "C3Atom+Helper.h"
C3Atom:
#import <Foundation/Foundation.h>
@interface C3Atom : NSObject{
//@protected支持属性的实例变量,只能在类层次结构中访问
@protected NSUInteger _protons; //质子数
@protected NSUInteger _neutrons; //中子数
@protected NSUInteger _electrons; //电子数
@protected NSString * _chemicalElement; //化学元素名
@protected NSString * _atomicSymbol; //原子符号
}
@property (nonatomic , readonly) NSUInteger protons;
@property (nonatomic , readonly) NSUInteger neutrons;
@property (nonatomic , readonly) NSUInteger electrons;
@property (nonatomic , readonly) NSString * chemicalElement;
@property (nonatomic , readonly) NSString * atomicSymbol;
-(NSUInteger)massNumber;
-(NSString *)logInfo;
@end
#import "C3Atom.h"
@implementation C3Atom
-(instancetype)init{
if (self = [super init]) {
_chemicalElement = @"None";
}
return self;
}
-(NSUInteger)massNumber{
return self.protons + self.neutrons;//原子的质量数等于质子数加中子数的和
}
-(NSString *)logInfo{
return NSStringFromClass([self class]);
}
@end
C3Hydrogen:
#import "C3Atom.h"
@interface C3Hydrogen : C3Atom
-(id)initWithNeutrons:(NSUInteger)neutrons;
+(id)hydrogenWithNeutrons:(NSUInteger)neutrons;
@end
#import "C3Hydrogen.h"
#import "C3HydrogenHelper.h"
@implementation C3Hydrogen{
@private C3HydrogenHelper * _helper;
}
-(id)initWithNeutrons:(NSUInteger)neutrons{
if (self = [super init]) {
_chemicalElement = @"Hydrogen";
_atomicSymbol = @"H";
_protons = 1;
_neutrons = neutrons;
//为实现消息转发机制创建辅助对象
_helper = [[C3HydrogenHelper alloc] init];
}
return self;
}
+(id)hydrogenWithNeutrons:(NSUInteger)neutrons{
return [[[self class] alloc] initWithNeutrons:neutrons];
}
//快速转发
-(id)forwardingTargetForSelector:(SEL)aSelector{
if ([_helper respondsToSelector:aSelector]) {
return _helper;
}
return nil;
}
//标准转发
/*
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
这个方法返回了对应的对象才会进
- (void)forwardInvocation:(NSInvocation *)anInvocation
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (signature==nil) {
signature = [_helper methodSignatureForSelector:aSelector];
}
NSUInteger argCount = [signature numberOfArguments];
for (NSInteger i=0 ; i<argCount ; i++) {
NSLog(@"%s" , [signature getArgumentTypeAtIndex:i]);
}
NSLog(@"returnType:%s ,returnLen:%ld" , [signature methodReturnType] , [signature methodReturnLength]);
NSLog(@"signature:%@" , signature);
return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if ([_helper respondsToSelector:[anInvocation selector]]){
[anInvocation invokeWithTarget:_helper];
}else{
[super forwardInvocation:anInvocation];
}
}
@end
C3HydrogenHelper:
#import <Foundation/Foundation.h>
@interface C3HydrogenHelper : NSObject
-(NSString *)factoid;
@end
#import "C3HydrogenHelper.h"
@implementation C3HydrogenHelper
-(NSString *)factoid{
return @"The lightest element!";
}
@end
C3Atom+Helper:
#import "C3Atom.h"
@interface C3Atom (Helper)
-(NSString *)factoid;
@end