在有些时候,我们希望能够用一些已经封装好的类,但又希望能够加点自己想要的功能,但是很明显,继承并不适用于一些工具包或者类库,如NSString,如果我们要给NSString加一个获得长度的方法该怎么办呢?这时候就要用到“类别”了。
零.写在前面:类别与继承
类别可以拓展一个类并添加额外的方法,使得在不修改该类原先代码的情况下,拓展或者修改现有类的定义,并且是向下有效的,会影响到该类的所有子类。
重写一个类的方式用继承还是分类取决于具体情况。
- 假如目标类有许多的子类,我们需要拓展这个类又不希望影响到原有的代码,继承比较好。
- 如果仅仅是拓展方法,分类更好。(不需要涉及到原先的代码)
2.分类:用来扩展类的方法,不能定义新成员,但是可以访问到私有成员
子类:可以通过覆盖和定义新方法来扩展父类,可以新增成员,但是不能访问父类的私有成员。
一.类别的创建与实现
1.创建
首先我们要声明一下"NSString"的类别,注意在NSString后面加个括号,只要保证类别名称的唯一性,可以向一个类中添加任意多的类别:
//NSString.h
#import <Foundation/Foundation.h>
@interface NSString(NumberConvenience)
-(NSNumber *) lengthAsNumber;
@end
//NSString.m
#import "NSString.h"
@implementation NSString (NumberConvenience)
- (NSNumber *) lengthAsNumber
{
unsigned int length = (int) [self length];
return [NSNumber numberWithUnsignedInt: length];
}
@end
2.实现
在Main.m里面调用该NSString:
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject: [@"hello" lengthAsNumber] forKey:@"hello"];
[dict setObject: [@"Hoben" lengthAsNumber] forKey:@"Hoben"];
NSLog(@"%@", dict);
可以看到,我们可以直接在NSString类后面添加lengthAsNumber方法。
3.局限性
- 无法向类中添加新的实例变量,即类别没有位置容纳实例变量。
- 当类别中的方法与现有的方法重名的时候,类别会具有更高的优先级,也就是说,我们创建的类别方法会完全取代初始方法,从而无法再使用初始方法。(解决:在类别方法名中加一个前缀,确保不发生名称冲突。)
二.类别的分散实现
我们可以将类别用于分散上面,即:将类的实现分散到多个不同文件或多个不同框架中,为什么要这样做?举个例子,NSString是Foundation框架中的一个类,包含了许多面向数据的类,但APPKit也有一个NSString的类别,称为NSStringDrawing,该类别允许我们向字符串对象发送绘图信息。Cocoa的设计人员使用类别将数据功能放在Foundation中实现,二将绘图功能放在APPKit实现。作为编程人员,我们只需处理NSString即可,通常不用关心特定的方法来自何处。
1.写在前面的注意事项
分类可以扩展类的方法,但是不能新创建类的对象,也就是说,用了分类之后,一切要使用的对象只能在父类中定义。(在本次实验中我遇到了@synthesize not allowed in a category's implementation的报错,所以特地提出来说一下)
2.实现
这次的实验我们将CategoryThing分为Thing1、Thing2、Thing3来实现。
//CategoryThing.h
#import <Foundation/Foundation.h>
@interface CategoryThing : NSObject
@property (nonatomic, assign) int thing1;
@property (nonatomic, assign) int thing2;
@property (nonatomic, assign) int thing3;
@end
//CategoryThing.m
#import "CategoryThing.h"
@implementation CategoryThing
- (NSString *)description
{
return [NSString stringWithFormat:@"%d %d %d",
thing1,
thing2,
thing3];
}
@end
再用不同的类别写CategoryThing的get和set方法:
#import "CategoryThing.h"
@interface CategoryThing (Thing1)
- (void) setThing1: (int) thing1;
- (int) thing1;
@end
...//还有implementation
最后在main.m文件实现:
CategoryThing *thing = [[CategoryThing alloc] init];
[thing setThing1: 3];
[thing setThing2: 5];
[thing setThing3: 7];
三.委托
protocol-协议,就是使用了这个协议后就要按照这个协议来办事,协议要求实现的方法就一定要实现。
delegate-委托,顾名思义就是委托别人办事,就是当一件事情发生后,自己不处理,让别人来处理。
当一个A view 里面包含了B view
B view需要修改A view界面,那么这个时候就需要用到委托了。
需要几个步骤:
1)首先定一个协议
2)A view实现协议中的方法
3)B view设置一个委托变量
4)把B view的委托变量设置成A view,意思就是,B view委托A view办事情。
5)事件发生后,用委托变量调用A view中的协议方法。
委托是一种对象,另一个类的对象会要求委托对象执行它的某些操作。例如,当APPKit类的NSApplication启动时,他会询问其委托对象是否应该打开一个无标题窗口。NSWindow类的对象询问它们自己的委托对象是否应该允许关闭某个窗口。
更常用的是,编写委托对象并将其提供给其他一些对象,通常是提供给Cocoa生成的对象。通过实现特定的方法,可以控制Cocoa中的对象的行为。
Cocoa中的滚动列表是由APPKit类的NSTableView处理的。当tableView对象准备好执行某些操作(例如选择用户刚刚点击的行)时,它询问其委托对象是否选择此行。tableView对象给其委托对象发送一条消息:
- (BOOL) tableView: (NSTableView *) tableView
shouldSelectRow: (int) row;
委托方法可以查看tableView对象和行并确定是否应该选择该行。如果表中包含了不该选择的行,则委托对象可能禁用那些不被选择的行。
现在以iTunesFinder项目为例,告诉网络服务浏览器你期待的服务并为其提供一个委托对象。然后,该浏览器对象将向委托对象发送消息,告知其发现新服务的时间。具体代码如注释所示。
NSNetServiceBrowser *browser;
browser = [[NSNetServiceBrowser alloc] init];
ITunesFinder *finder;
finder = [[ITunesFinder alloc] init];
//告诉网络服务浏览器,使用ITunesFinder类的对象作为双亲委托
[browser setDelegate: finder];
//"_daap._tcp"告诉网络服务浏览器使用TCP网络协议去搜索DAAP(数字音频访问协议)
//类型的服务。该语句可以找出由iTunes发布的库。
//域local.表示只在本地网络中搜索该服务。
[browser searchForServicesOfType: @"_daap._tcp"
inDomain: @"local."];
NSLog(@"begun browsing");
//run循环会一直处于阻塞状态(不执行任何处理),直到某些有趣的事情发生为止。
//在本例中,有趣的事情是指网络服务浏览器发现了新的iTunes共享。
[[NSRunLoop currentRunLoop] run];
在ITunesFinder类中,我们并不需要在@interface中声明方法。要成为一个委托对象,我们只需实现已经打算调用的方法。
//ITunesFinder.h
#import <Foundation/Foundation.h>
//这里还是要声明一下该类是NSNetServiceBrowser的委托,否则会有warning
@interface ITunesFinder : NSObject <NSNetServiceBrowserDelegate>
@end
//ITunesFinder.m
@implementation ITunesFinder
-(void) netServiceBrowser: (NSNetServiceBrowser *) b
didFindService:(nonnull NSNetService *)service
moreComing:(BOOL)moreComing
{
//当NSNetServiceBrowser发现新服务时,他给委托对象发送netServiceBrowser:didFindService:moreComing:消息
//浏览器被作为第一个参数(与main()函数中Browser变量的值相同)传递
//如果有多个服务浏览器在同时进行搜索,可以利用参数检查计算出哪个浏览器发现了新服务。
//NSNetService描述了被发现的服务(如ITunes共享)
//moreComing用于指示一批通知是否已经完成。
[service resolveWithTimeout: 10];
NSLog(@"found one! Name is %@", [service name]);
}
-(void) netServiceBrowser: (NSNetServiceBrowser *) b
didRemoveService:(nonnull NSNetService *) service
moreComing:(BOOL)moreComing
{
//在某个服务不可再用的时候被调用。
[service resolveWithTimeout: 10];
NSLog(@"lost one! Name is %@", [service name]);
}
@end
四.委托和类别
被发送给委托对象的方法可以声明为一个NSObject的类别,如NSNetService委托方法的部分声明如下:
#import <Foundation/Foundation.h>
@interface NSObject(NSNetServiceBrowserDelegateMethods)
- (void) netServiceBrowserWillSearch:
(NSNetServiceBrowser *) browser;
- (void) netServiceBrowser: (NSNetServiceBrowser *) aNetServiceBrowser
didFindService: (nonnull NSNetService *)service
moreComing: (BOOL)moreComing;
- (void) netServiceBrowserDidStopSearch:
(NSNetServiceBrowser *)browser;
- (void) netServiceBrowser: (NSNetServiceBrowser *) browser
didRemoveService: (nonnull NSNetService *)service
moreComing: (BOOL)moreComing;
@end
通过将这些方法声明为NSObject的类别,NSNetServiceBrowser的实现可以将这些消息之一发送给任何对象,无论这些对象实际上属于哪个类。这也意味着,只要对象实现了委托方法,任何类的对象都可以成为委托对象。
创建一个NSObject的类别成为“创建一个非正式协议”,即实现自己可能希望实现的方法,使用它们更好地完成工作。之后如果想使用该方法,则在main.m中绑定委托对象,再在委托对象所在的类实现该方法,即可委托完成。
五.响应选择器
当NSNetServiceBrowser试图发送一个对象无法理解的消息的时候,就会发生OC的运行时错误:selector not recognized。
解决:NSNetServiceBrowser首先检查对象,询问其能否响应该选择器。如果对象能够响应该选择器,NSNetServiceBrowser则给他发送消息。
如:
if ([browser respondsToSelector:
@selector(netServiceBrowser:didFindService:moreComing:)])
如果该委托对象能够响应给定的消息,则浏览器向该对象发送此消息。否则,浏览器将暂时忽略该委托对象,继续正常运行。
六.协议
正式协议是一个命名的方法列表。但与非正式协议不同的是,正式协议要求显式地采用协议。即在类的@interface声明中列出协议的名称。采用协议意味着承诺实现该协议的所有方法。否则,编译器将会生成警告。
1.声明协议
我们声明一个协议叫做NSCopying,如果采用该协议,则对象将知道如何复制自己:
@protocol NSCopying
- (id) copyWithZone: (NSZone *) zone;
@end
这和interface有点像,但区别在于:@protocol告诉编译器:“下面将是一个新的正式协议”,@protocol之后是协议名称,协议名称必须唯一。
2.复制
copy消息通知对象创建一个全新的对象,并使新对象与接收copy消息的原对象一样。下面进行Car的复制实验:
//Engine.h
@interface Engine : NSObject <NSCopying>
//Engine.m
- (id) copyWithZone:(NSZone *)zone
{
Engine *engineCopy;
//首先通过[self class]获得self对象所属的类
//然后通过allocWithZone分配内存并创建一个该类的新对象
//最后给新对象发送init消息使其初始化
engineCopy = [[[self class] allocWithZone: zone] init];
return engineCopy;
}
//Tire.h
@interface Tire : NSObject <NSCopying>
//Tire.m
- (id) copyWithZone:(NSZone *)zone
{
Tire *tireCopy;
tireCopy = [[[self class]
allocWithZone: zone]
initPressure: _pressure andTreadDepth:_treadDepth];
return tireCopy;
}
实现继承的类的时候,无需声明<NSCopying>,可以直接调用父类的方法。
//AllWeatherRadial.m
- (id) copyWithZone:(NSZone *)zone
{
AllWeatherRadial *tireCopy;
tireCopy = [super copyWithZone: zone];
[tireCopy setRainHandling: _rainHandling];
[tireCopy setSnowHandling: _snowHandling];
return tireCopy;
}
复制Car本身:
- (id) copyWithZone:(NSZone *)zone
{
Car *carCopy;
carCopy = [[[self class] allocWithZone: zone] init];
carCopy.name = self.name;
Engine *engineCopy = [engine copy];
carCopy.engine = engineCopy;
for (int i = 0; i < 4; i++) {
Tire *tireCopy;
tireCopy = [[self tireAtIndex: i] copy];
[carCopy setTire: tireCopy atIndex: i];
}
return carCopy;
}
在main.m这样调用:
Car *carCopy = [car copy];
[carCopy print];
即可复制之前创建好的Car类实例。
3.协议与复制类型
我们可以在使用的数据类型中为实例变量和方法参数指定协议名称,这样就可以给OC的编译器提供更多一点的信息,从而有助于检查代码中的错误,如:
- (void) setObjectValue: (id<NSCopying>) obj;
编译器将会直到期望的任意类型的对象,只要其遵守该协议。