从零开始——我的OC学习之路

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

2018-05-28  本文已影响36人  肠粉白粥_Hoben

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

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

上一篇 下一篇

猜你喜欢

热点阅读