2018-05-28 类别、委托与协议

在有些时候,我们希望能够用一些已经封装好的类,但又希望能够加点自己想要的功能,但是很明显,继承并不适用于一些工具包或者类库,如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;

编译器将会直到期望的任意类型的对象,只要其遵守该协议。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,304评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,086评论 1 32
  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 829评论 0 4
  • 1.项目经验 2.基础问题 3.指南认识 4.解决思路 ios开发三大块: 1.Oc基础 2.CocoaTouch...
    阳光的大男孩儿阅读 4,969评论 0 13
  • 梧桐叶落潇雨 风与谁轻舞渐逝的锦瑟年华 回忆里仍忆起撑着油纸伞 走过的青石板陌 跫音如春雨错落于芭蕉叶 渺渺红尘 ...
    琦雪冰凝阅读 291评论 0 5