目录
OC的包装类
OC提供了NSValue、NSNumber来封装C语言的基本类型,这样就可以让ta们具有面向对象的特征。
ta们不是包装类
在进行iOS开发时,可能会遇到如下3个类型:
- NSInteger:大致等于long型整数。
- NSUInteger:大致等于unsigned long型整数。
- CGFloat:在64位平台上大致相当于double,在32位平台上大致相当于float。
千万不要被上面的NS、CG前缀迷惑了,ta们并不是包装类,ta们依然只是基本类型。
为了更好地兼容不同的平台,当程序需要定义整型变量时,建议使用NSInteger、NSUInteger;当程序需要定义浮点型变量时,建议使用CGFloat。
NSValue和NSNumber
NSvalue和NSNumber都是包装类,其中NSValue时NSNumber的父类。
NSValue代表一个更通用的包装类,ta可用于包装单个short
、int
、long
、float
、char
、指针、对象id
等数据项,通过该包装类NSVlaue
,就可以把以上的基本数据类型添加到NSArray、NSSet等集合(这些集合要求ta们的元素必须是对象)中。
NSNumber是更具体的包装类,主要用于包装C语言的各种数据类型,NSNumber主要包括如下3类方法:
+ numberWithXxx:
:该类方法直接将特定类型的值包装成NSNumber。- initWithXxx:
:该实例方法需要先创建一个NSNumber对象,再用一个基本类型的值来初始化NSNumber。- xxxVlaue:
:该实例方法返回该NSNumber对象包装的基本类型的值。
第3类方法用于从包装类中取出基本类型的值。
下面程序示范了基本类型的值与包装类之间的转换:
#import <Foundation/Foundation.h>
int main(int argc,char* argv[]) {
@autoreleasepool {
//调用类方法将int、double类型包装成NSNumber对象
NSNumber* num = [NSNumber numberWithInt: 20];
NSNumber* de = [NSNumber numberWithDouble: 3.7];
NSLog(@"%d", [num intVlaue]);
NSLog(@"%g", [de doubleValue]);
NSNumber* ch = [[NSNumber alloc] initWithChar: 'J'];
NSLog(@"%@", ch);
}
}
从上面的程序可以看出,将基本类型的值转换为包装类简单的做法就是调用numberWithXxx:
类方法*,调用该方法时传入一个基本类型的值,该方法就会返回包装该值的包装类实例。
基本类型变量和包装类对象之间的转换关系如下:
虽然OC也提供了类似于自动装箱的机制,比如,可以直接把一个整型值赋给NSNumber变量。但这种机制并不完善,**使用自动装箱生成的NSNumber不支持ARC,而且不能把浮点数赋给NSNumber类型的变量。**因此,通常建议显式地将基本类型的值包装成NSNumber对象。
处理对象
OC对象都是NSObject子类的实例,都可直接调用该类中定义的方法,这些方法提供了处理OC对象的通用方法。
打印对象和description方法
当使用NSLog()
函数输出OC对象时,输出的是该对象的description方法的返回值。也就是说,下面两行代码的效果完全一样:
NSLog(@"%@", p);
NSLog(@"%@", [p description]);
description方法是NSObject类的一个实例方法,因此,所有的OC对象都具有description方法。
description方法是一个非常特殊的方法,ta是一个**“自我描述”**方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。
NSObject类提供的description方法总是返回<THGPerson: 十六进制的首地址>
,这个返回值并不能真正实现“自我描述”功能,因此,如果用户需要自定义类实现“自我描述”的功能,则必须重写NSObject类的description方法。
大部分时候,重写descrip方法总是可以返回该对象所有令人感兴趣的信息所组成的字符串,通常可返回如下格式的字符串:
<类名[实例变量1=值1, 实例变量2=值2, …]>
看以下示例:
#import "THGApple.h"
@implementation THGApple
- (id)initWithColor: (NSString*)color weight: (double)weight {
if (self = [super init]) {
self.color = color;
self.weight = weight;
}
return self;
}
- (NSString*)description {
return [NSString stringWithFormat: @"<THGApple[_color=%@, _weight=%g]>", self.color, self.weight];
}
@end
重写了THGApple的description方法之后,程序实际上输出的是THGApple对象的description方法的返回值。
#import <Foundation/Foundation.h>
#import "THGApple.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
THGApple* a = [[THGApple alloc] initWithColor: @"红色" weight: @"5.67"]; //创建一个THGApple对象,将之赋给a变量
NSLog(@"%@", a);
}
}
编译、运行,结果如下:
<THGApple[_color=红色, _weight=5.67]>
通过重写THGApple类的description方法,就可以让系统在打印THGApple对象时打印出该对象的“自我描述”信息。
==和isEqual:方法
在OC中测试两个变量是否相等有两种方式:一种是利用==
运算符;另一种是利用isEqual:
方法。
当使用==
来判断两个变量是否相等时,如果两个变量是基本类型,且都是数值型(不一定要求数据类型严格相同),则只要两个变量的值相等,使用==
判断就返回真。
对于两个指针类型的变量,ta们必须指向同一个对象(也就是两个指针变量保存的内存地址相同)时,使用==
判断才返回真。当使用==
比较类型上没有继承关系的两个指针变量时,编译器会提示警告。
#import <Founadation/Foundation.h>
int main(int argc, char* argv[]) {
@autoreleasepool {
int it = 65;
float f1 = 65.0f;
NSLog(@"65和65.0f是否相等?:%d", (it == f1)); //1
char ch = 'A';
NSLog(@"65和'A'是否相等?:%d", (it == ch)); //1
NSString* str1 = [NSString stringWithFormat: @"Hello, iOS!"];
NSString* str2 = [NSString stringWithFormat: @"Hello, iOS!"];
NSLog(@"str1和str2是否相等?:%d", (str1 == str2)); //0
NSLog(@"str1是否isEqual: str2?:%d", [str1 isEqual: str2]); //1
NSLog(@"%d", [NSDate new] == [NSString new]); //由于NSDate与NSString类没有继承关系,所以此句会导致编译警告
}
}
NSString还有一个非常容易迷惑的地方:@"Hello, iOS!"
直接量和[NSString stringWithFormat: @"hello"]
有什么区别呢?
当OC程序直接使用字符串直接量时,系统将会使用常量池来管理这些字符串。
下面的程序示范了系统使用常量池管理字符串直接量的情形:
#import <Foundation/Foundation.h>
int main(int argc, char* argv[]) {
@autoreleasepool {
NSString* s1 = @"Hello, iOS!";
NSString* s2 = @"Hello, iOS!";
NSLog(@"s1地址:%p,s2地址:%p", s1, s2);
NSLog(@"s1与s2是否相等:%d", (s1 == s2)); //1
NSString* s3 = [NSString stringWithFormat: @"Hello, iOS!"];
NSLog(@"s3地址:%p", s3);
NSLog(@"s1与s3是否相等:%d", (s1 == s3)); //0
}
}
常量池保证相同的字符串直接量只有一个,不会产生多个副本。
使用NSString的stringWithFormat:
类方法创建的额字符串对象时运行时创建出来的,ta被保存在运行时内存区(即堆内存)内,不会放入常量池中。
默认情况下,NSObject提供的isEqual:
方法判断两个对象相等的标准与==
运算符没有区别,同样要求两个指针变量指向同一个对象才会返回真。因此,NSObject类提供的isEqual:
方法没有太大的实际意义,如果希望采用自定义的相等标准,则可通过重写isEqual:
方法来实现。
NSString已经重写了NSObject的isEqual:
方法,NSString的isEqual:
方法判断两个字符串相等的标准是只要两个字符串所包含的字符序列相同,通过isEqual:
比较就将返回真;否则将返回假。
重写
isEqual:
方法就是提供自定义的相等标准。在极端情况下,你可以让THGDog对象和THGItem对象相等。
我们希望两个类型相同的对象才可能相等,而且必须两个对象的关键属性相等才算相等。看下面重写THGUser类的isEqual:
方法:
#import <Foundation/Foundation.h>
@interface THGUser : NSObject
@property (nonatomic, copy)NSString* name;
@property (nonatomic, copy)NSString* idStr;
- (id)initWithName: (NSString*)name idStr: (NSString*)idStr;
@end
上面定义了THGUser类,该类包含两个属性:name和idStr。假如系统业务要求:只要两个THGUser的idStr相等,即可认为两个THGUser对象相等。
THGUser类的实现部分:
#import "THGUser.h"
@implementation THGUser
- (id)initWithName: (NSString*)name idStr: (NSString*)idStr {
if (self = [super init]) {
self.name = name;
self.idStr = idStr;
}
retrun self;
}
- (BOOL)isEqual: (id)other {
if (self == other) return YES; //两个对象为同一个对象
if (other != nil && [other isMemberOfClass: THGUser.class]) { //当other不为nil,且ta是THGUser类的实例时
THGUSer* target = (THGUSer*)other;
return [self.idStr isEqual: target.idStr];
}
return NO;
}
@end
测试以上THGUser类:
#import <Foundation/Foundation.h>
#import "THGUser.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
THGUSer* p1 = [[THGUser alloc] initWithName: @"LJY" idStr: @"12343433433"];
THGUser* p2 = [[THGUser alloc] initWithName: @"IN" idStr: @"12343433433"];
THGUser* p3 = [[THGUser alloc] initWithName: @"SEVENSEVEN" idstr: @"99933433"];
NSLog(@"p1和p2是否相等?%d", [p1 isEqual: p2]); //1
NSLog(@"p2和p3是否相等?%d", [p2 isEqual: p3]); //0
}
}
通常而言。正确地重写isEqual:
方法应该满足下列条件:
- 自反性:对任意x,
[x isEqual: x]
一定返回真。 - 对称性:对任意x和y,如果
[y isEqual: x]
返回真,则[x isEqual: y]
也返回真。 - 传递性:对任意x、y、z,如果有
[x isEqual: y]
返回真,[y isEqual: z]
返回真,则[x isEqual: z]
一定返回真。 - 对任意x和y,如果对象中用于等价比较的关键属性没有改变,那么无论调用
[x isEqual: y]
多少次,返回的结果都应该保持一致,要么一直是真,要么一直是假。 - 对任何不是nil的x,
[x isEqual: nil]
一定返回假。
NSObject默认提供的isEqual:
只是比较对象的地址,即NSObject类的isEqual:
方法比较的结果与==
运算符比较的结果完全相同。因此,在实际应用中常常需要重写isEqual:
方法,重写时,等价条件是由业务要求决定的。
在以后的iOS开发中,还会看到程序中使用
isEqualToString:
方法来判断两个字符串相等的代码。实际上,NSString不仅重写了**isEqual:
方法,用于判断两个字符串的字符序列是否相等**,还定义了一个**isEqualToString:
方法,该方法专门用于判断当前字符串与另一个字符串的字符序列是否相等**。
类别与扩展
通过继承,子类可以在父类的基础上添加新的方法,甚至重写父类已有的方法。
但有些时候,使用继承并不是最好的选择,比如希望为NSNumber类新增一些方法,但由于NSNumber实际上只是一个类蔟(cluster)的前端类,例如,通过[NSNumber numberWithInt: 5]
方法所生成的NSNumber对象其实只是NSNumber子类的实例。这样即使为NSNumber派生子类也没有任何意义,派生的子类对NSNumber现有的子类并没有任何影响。此时就需要借助**类别(category)**来实现。
由于OC并没有提供抽象类的语法支持,而在实际项目的开发中,总有需要抽象类的时候,此时就会选择定义一个父类,并以该父类派生多个子类,其他程序使用这些类时,总是面向父类编程,当程序调用父类的初始化方法、类方法来返回对象时,实际上返回的是子类的实例。这一系列的类被称为一个类蔟(cluster),这个父类也就模拟了抽象类的功能。
类别(category)
OC的动态特征允许使用类别为现有的类添加新方法,并且不需要创建子类,不需要访问原有类的源代码。
通过使用类别即可动态地为现有的类添加新方法,而且可以将类定义模块化地分布到多个相关文件中。
类别同样由接口和实现部分组成,类别接口部分及实现部分的语法如下:
@interface 已有类 (类别名)
//方法定义
...
@end
@implementation 已有类 (类别名)
// 方法实现
...
@end
定义类别的语法与定义类的语法存在如下差异:
- 定义类时使用的类名必须是该项目中没有的类,而定义类别时使用的类名必须是已有的类。
- 定义类别时必须使用原括号来包含类别名。
- 类别中通常只定义方法。
下面考虑为NSNumber增加一个类别,类别接口部分如下:
@interface NSNumber (thg)
- (NSNumber*) add: (double)num2;
- (NSNumber*) substract: (double)num2;
- (NSNumber*) multiply: (double)num2;
- (NSNumber*) divide: (double)num2;
@end
就编程习惯来说,一般习惯将类别的接口文件命名为**“类名+类别名.h”的形式,与此类似,类别实现部分的文件通常命名为“类名+类别名.m”**的形式。
接下来为该类别提供实现部分:
#import "NSNumber+thg.h"
@implementation NSNumber (thg)
- (NSNumber*)add: (double)num2 {
return [NSNumber numberWithDouble: ([self doubleValue] + num2)];
}
- (NSNumber*)substract: (double)num2 {
return [NSNumber numberWithDouble: ([self doubleValue] + num2)];
}
- (NSNumber*)multiply: (double)num2 {
return [NSNumber numberWithDouble: ([self doubleValue] * num2)];
}
- (NSNumber*)divide: (double)num2 {
return [NSNumber numberWithDouble: ([self doubleValue] / num2);
}
上面为NSNumber定义了thg类别,接下来只要在程序中导入NSNumber+thg.h头文件,并在测试程序中使用NSNumber类,该类的实例就会具有以上实现的4个方法,这就实现了对原有NSNumber类的动态扩展。
#import <Foundation/Foundaton.h>
#import "NSNumber+thg.h" //已通过thg类别对NSNumber类进行了动态扩展
int main(int argc, char* argv[]) {
@autoreleasepool {
NSNumber* myNum = [NSNumber numberWithInt: 7];
NSNumber* add = [myNum add: 2.7];
NSLog(@"%@", add);
NSNumber* substract = [myNum substract: 2.7];
NSLog(@"%@", substract);
NSNumber* multiply = [myNum multiply: 2.7];
NSLog(@"%@", mutiply);
NSNumber* divide = [myNum divide: 2.7];
NSLog(@"%@", divide);
}
}
虽然类别可以重写原有类中的方法,但通常并不推荐这么做,如果需要重写原有类的方法,更好的建议是通过原有类派生子类,然后在子类中重写父类原有的方法。
关于类别,还有如下两点说明:
- 通过类别为指定类添加新方法之后,这个新方法不仅会影响NSNumber类,还会影响NSNumber类的所有子类,每个子类都会获取类别扩展的方法。
- 可根据需要为一个类定义多个类别,不同类别都可对原有的类增加方法定义。
类别是OC中一个非常重要的知识,ta通常有如下3种用法:
- 利用类别对类进行模块化设计。
- 使用类别来调用私有方法。
- 使用类别来实现非正式协议。
利用类别对类进行模块化设计
在前面类的设计中,不能将类实现部分分布到多个*.m
文件中。
当某个类非常大时,如果将该类的所有实现代码放在一个文件中,将会导致这个文件非常大,以至于维护起来非常困难。如果需要将一个较大的类分模块设计,使用类别是一个不错的选择。
下面以AppKit提供的NSWindow类为例来介绍利用类别对类进行模块化设计的方式:
在Xcode中查看头文件NSWindow.h
,可以看到NSWindow的定义如下:
@interface NSWindow : NSResponder <NSAnimatablePropertyContainer, NSUserInterfaceValidaitons, NSUserInterfaceItemIdentification>
还可以看到如下类别:
@interface NSWindow (NSKeyboardUI)
@interface NSWindow (NSToolbarSupport)
@interface NSWindow (NSDrag)
@interface NSWindow (NSCarbonExtensions)
从上面类的接口定义和类别的接口定义来看,通过这种方式,NSWindow可以分别提供NSWindow.m
、NSWindow+NSKeyboardUI.m
、NSWindow+NSToolbarSupport.m
、NSWindow+NSDrag.m
和NSWindow+NSCarbonExtensions.m
实现文件,通过这种方式就可以对类实现按模块分布到不同的*.m
文件中,从而提高项目后期的可维护性。
使用类别来调用私有方法
没有在接口部分定义而是在类实现部分定义的方法相当于私有方法,通常不允许被调用。但OC实际上没有真正私有的方法,如果使用NSObject的performSelector:
方法来执行动态调用,则完全可以调用那些私有方法。
使用performSelector:
方法来执行调用,则完全避开了编译器的语法检查,有时候未必是一种好的做法。其实还可以通过类别来定义前向引用,从而实现对私有方法的调用。
下面假设calDiscount:
方法是一个私有方法:
如果直接通过该对象调用这个私有方法,编译器会提示报错(OC编译器习惯把方法称为selector)。
为了能在该程序中正常调用calDiscount
方法,程序可以在该main()
函数前增加如下类别定义:
@interface THGItem (thg)
- (double)calDiscount: (double)discount; //直接声明该方法
@end
定义了此类别后,类别的方法并不强制程序去实现ta——实际上,THGItem类的实现部分已经实现了calDiscount:
方法。
扩展(extension)
扩展与类别相似,扩展相当于匿名类别,定义扩展的语法如下:
@interface 已有类 () {
//实例变量
}
// 方法定义
...
@end
扩展相当于定义了一个匿名的类别。但就用法来看,类别通常有单独的*.h
和*.m
文件,扩展则用于临时对于某个类的接口进行扩展,类实现部分同时实现类接口部分定义的方法和扩展中定义的方法。
在定义类的扩展时,可以额外增加实例变量,也可以使用@property
来合成属性(包括setter、getter方法和对应的成员变量),但定义类的类别时,则不允许额外定义实例变量,也不能用@property
合成属性。
下面先定义THGCar类的接口部分:
#import <Foundation/Foundation.h>
@interface THGCar : NSObject
@property (nonatomic, copy)NSString* brand;
@property (nonatomic, copy)NSString* model;
- (void)drive;
@end
接下来对该类进行扩展,扩展的接口部分如下:
#import "THGCar.h"
@interface THGCar ()
@property (nonatomic, copy)NSString* color;
- (void)drive: (NSString*)owner;
@end
THGCar类的实现部分:
#import "THGCar+drive.h"
@implementation THGCar
- (void)drive {
NSLog(@"%@汽车正在路上奔驰", self);
}
- (void)drive: (NSString*)owner {
NSLog(@"%@正驾驶%@汽车在路上奔驰", owner, self);
}
- (NSString*)description {
return [NSString stringWithFormat: @"<THG[_brand=%@,_model=%@,_color=%@]>", self.brand, self.model, self.color];
}
@end
测试THGCar类:
#import <Foundation/Foundation.h>
#import "THGCar+drive.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
THGCar* car = [[THGCar alloc] init];
car.brand = @"宝马";
car.model = @"X5";
car.color = @"黑色";
[car drive];
[car drive: @"Kevince"];
}
}
协议(protocol)与委托
协议的作用类似于接口,用于定义多个类应该遵循的规范。
规范、协议与接口
下图显示了规范、类和实例的抽象示意图。
同一个类的内部状态数据、各种方法的实现细节完全相同,类时一种具体的实现体。而协议则定义了一种规范,协议定义某一批类所需要遵循的规范,ta不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,ta只规定这批类中必须提供某些方法,提供这些方法的类就可满足实际需要。
协议不提供任何实现。协议体现的是规范和实现分离的设计哲学。
让规范和实现分离正是协议的好处,是一种松耦合的设计。例如,主机板上提供了PCI插槽,只要一块显卡遵循PCI协议的规范,就可以插入PCI插槽内,与该主机版正常通信。至于这块显卡时哪个厂家制造的,以及内部是如何实现的,主机板无须关心。
OC中协议的作用就相当于其他语言中接口的作用。
协议定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着协议里通常是定义一组共用方法,但不会为这些方法提供实现,方法的实现则交给类去完成。
使用类别实现非正式协议
当某个类实现NSObject的该类别时,就需要实现该类别下的所有方法,这种基于NSObject定义的类别即可认为是非正式协议。
下面以NSObject为基础,定义了一个类别,类别名称为Eatable:
#import <Foundation/Foundation.h>
@interface NSObject (Eatable)
- (void)taste;
@end
接下来所有继承NSObject类的子类都会自动带有该方法,而且NSObject的子类可以根据需要,自行决定是否要实现该方法。
既然Eatable类别作为一个非正式协议使用,那么相当于定义了一个规范,因此,遵守该协议的子类通常都会实现这个方法。
接下来为NSObject(Eatable)派生一个子类:
#import <Foundation/Foundation.h>
#import "NSObject+Eatable.h"
@interface THGApple : NSObject
@end
该THGApple子类虽然是一个空类,但ta继承了NSObject(Eatable),只要在THGApple类的实现部分实现taste方法即可:
#import "THGApple.h"
@implementation THGApple
- (void)taste {
NSLog(@"苹果营养丰富,口味很好!");
}
@end
THGApple类实现了taste方法,这样THGApple类就相当于遵守了Eatable协议,接下来就可以把THGApple类当成Eatable对象来调用。
测试THGApple类:
#import <Foundation/Foundation.h>
#import "THGApple.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
THGApple* app = [[THGApple] alloc] init];
[apple taste];
}
}
THGApple对象遵循了Eatable协议,因此,可以调用taste方法。
最后需要指出的是,对于实现非正式协议的类而言,OC并不强制实现该协议中的所有方法。
但如果该类没有实现非正式协议中的某个方法,那么程序运行时调用该方法,就会引发unrecognized selector错误。
正式协议的定义
正式协议不在使用@interface
、@implementation
关键字,而是使用@protocol
关键字。定义正式协议的基本语法如下:
@protocol 协议名 <父协议1, 父协议2> {
//零个到多个方法定义...
}
- 协议名应与类名采用相同的命名规则。
- 一个协议可以有多个直接父协议,但协议只能继承协议,不能继承类。
- 协议中定义的方法只有方法签名,没有方法实现;协议中包含的方法既可是类方法,也可是实例方法。
协议定义的是多个类共同的公共行为规范,因此,协议里所有方法都具有公开的访问权限。
下面定义一个协议:
@protocol THGOutput
- (void)output;
- (void)addData: (NSString*)msg;
@end
这就定义了THGOutput协议的规范:只要某个类能添加数据,并可以将数据输出,那ta就是一个输出设备,至于这个设备的实现细节,该协议并不关心。
接下来再定义一个协议:
#import <Foundation/Foundation.h>
@protocol THGProductable
- (NSDate*)getProduceTime;
@end
这表明:无论是何种产品,都应该提供一个getProduceTime方法来获取该产品的生产时间。
接下来定义一个打印机协议,该协议同时继承上面两个协议:
#import <Foundation/Foundation.h>
#import "THGOutput.h"
#import "THGProductable.h"
@protocol THGPrintable <THGOutput, THGProductable>
- (NSString*)printColor;
@end
协议的继承和类继承不一样,协议完全支持多继承,即一个协议可以有多个直接的父协议。和类继承相似,子协议继承某个父协议,将会获得父协议里定义的所有方法。
遵守(实现)协议
在类定义的接口部分可指定该类继承的父类,以及遵守的协议,语法如下:
@interface 类名 : 父类 <协议1, 协议2…>
一个类可以同时遵守多个协议。
由于OC协议的功能基本等同于接口,因此,有时候也把遵守协议说成实现协议。实际上,OC编译器也使用implement作为实现协议的说法。
下面为THGPrintable协议提供一个实现类:THGPrinter,该实现类的接口部分如下:
#import <Foundation/Foundation.h>
#import "THGPrintable.h"
@interface THGPrinter : NSObject <THGPrintable>
@end
THGPriner类实现部分:
#import"THGPrinter.h"
#define MAX_CACHE_LINE 10
@implementation THGPrinter {
NSString* printData[MAX_CACHE_LINE]; //记录所有需要缓存的打印数据
int dataNum; //记录当前需打印的作业数
}
- (void)output {
while (dataNum > 0) {
NSLog(@"打印机使用%@打印:%@", self.printColor, printData[0]);
dataNum--; //作业数减1
for (int i = 0; i < dataNum; ++i) { //将作业队列整体前移一位
printData[i] = prtinData[i + 1];
}
}
}
- (void)addData: (NSString*)msg {
if (dataNum >= MAX_CACHE_LINE) {
NSLog(@"输出队列已满,添加失败");
} else {
printData[dataNum++] = msg; //未满就添加数据
}
}
- (NSDate*)getProduceTime {
return [[NSData alloc] init];
}
- (NSString*)printColor {
return @"红色";
}
@end
假如实现类未实现协议中的printColor方法,编译器就会报错。如果实现类实现了协议中的所有方法,那么程序就可以调用该实现类所实现的方法。
THGPrinter类测试代码:
#import <Foundation/Foundation.h>
#import "THGPrinter.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
THGPrinter* printer = [[THGPrinter alloc] init];
[printer addData: @"Hello, iOS!"];
[printer addData: @"Hello, XML!"];
[printer output];
[printer addData: @"Hello, Android!"];
[printer adddata: @"Hello, Ajax!"];
[printer output];
//创建一个THGPrinter对象,当成THGProductable使用
NSObject<THGProductable>* p = [[THGPrinter alloc] init];
NSLog(@"%@", p.getProduceTime);
//创建一个THGPrinter对象,当成THGOutput使用
id<THGOutput> out = [[THGPrinter alloc] init];
[out addData: @"SEVEN"];
[out addData: @"qi"];
[out output];
}
}
上面代码中使用协议来定义的变量只能调用该协议中声明的方法,否则编译器会提示错误。
使用协议来定义变量的语法如下:
NSObject<协议1, 协议2...>* 变量;
id<协议1, 协议2...> 变量;
通过上面的语法定义的变量,ta们的编译时类型仅仅是所遵守的协议类型,因此只能调用该协议中定义的方法。
对比正式协议与非正式协议,不难发现存在如下差异:
- 非正式协议通过为NSObject创建类别来实现;而正式协议则直接使用
@protocol
创建。 - 遵守非正式协议通过继承带特定类别的NSObject来实现;而遵守正式协议则必须实现协议中定义的所有方法。
为了弥补遵守正式协议必须实现协议的所有方法造成的灵活性不足,现新增了@optional
、@required
两个关键字:
@optional
:位于该关键字之后、@optional
或@end
之前声明的方法是可选的——实现类既可选择实现这些方法,也可不实现这些方法。@required
:位于该关键字之后,@required
或@end
之前声明的方法是必需的——实现类必须实现这些方法。如果没有实现这些方法,编译器就会提示警告。@required
是默认行为。
例如,协议如下:
@protocol THGOutput
@optional
- (void)output;
@required
- (void)addData: (NSString*)msg;
@end
该协议的实现类可选实现output方法,但必须实现addData:
方法。
通过在正式协议中使用@optional
、@required
关键字,正式协议完全可以代替非正式协议的功能。
协议与委托(delegate)
协议体现的是一种规范,定义协议的类可以把协议定义的方法委托给实现协议的类,这样可以让类定义具有更好的通用性,因为具体的动作将由该协议的实现类去完成。
例如,当应用程序启动时,应用程序的开始加载、加载完成等一系列事件,都是委托给相应的代理对象负责处理的。