Categories and Extensions in Objective-C

前言

类别是Objective-C语言的一种特性,容许程序员向现有类添加新方法,就像C#的extension同样。 可是,不要将C#中的extension和Objective-C的extension混淆。 Objective-C的extension是categories的特例,extension必须定义在.m文件中。程序员

extension和categories功能强大,具备许多潜在用途。 主要有如下三种:objective-c

  1. 首先,categories能够将类的接口和实现分红几个文件,这为大型项目提供了模块化的可能。
  2. 其次,categories容许程序员修复现有类(例如,NSString)中的错误,而无需对其进行子类化。
  3. 第三,实现了相似于C#和其余Simula类语言中的protected和private方法。

Categories

categories是同一个类的一组相关方法,categories中定义的全部方法均可以经过类得到,就好像它们是在.h文件中定义的同样。 举个例子,参考Person类。 若是这是一个大型项目,Person可能有许多方法,从基本行为到与其余人的交互到身份检查。 API可能要求经过单个类提供全部这些方法,但若是每一个组都存储在单独的文件中,则开发人员能够更轻松地进行维护。 此外,categories消除了每次更改单个方法时从新编译整个类的须要,这能够节省大型项目的时间。编程

咱们来看看如何使用categories来实现这一目标。 咱们从一个普通的类接口和相应的实现开始:编程语言

// Person.h
@interface Person : NSObject
 
@property (readonly) NSMutableArray* friends;
@property (copy) NSString* name;
 
- (void)sayHello;
- (void)sayGoodbye;
 
@end
 
 
// Person.m
#import "Person.h"
 
@implementation Person
 
@synthesize name = _name;
@synthesize friends = _friends;
 
-(id)init{
    self = [super init];
    if(self){
        _friends = [[NSMutableArray alloc] init];
    }
 
    return self;
}
 
- (void)sayHello {
    NSLog(@"Hello, says %@.", _name);
}
 
- (void)sayGoodbye {
    NSLog(@"Goodbye, says %@.", _name);
}
@end
复制代码

这里没什么新东西 - 只有一个具备两个属性的Person类(咱们的categories将使用friends属性)和两个方法。 接下来,咱们将使用一个categories来存储一些与其余Person实例交互的方法。 建立一个新文件,但不使用类,而是使用Objective-C Category模板。ide

Figure 28 Creating the PersonRelations class

正如所料,这将建立两个文件:用于保存接口的头文件和实现文件。 可是,这些看起来与咱们一直在使用的略有不一样。 首先,咱们来看看界面:模块化

// Person+Relations.h
#import <Foundation/Foundation.h>
#import "Person.h"
 
@interface Person (Relations)
 
- (void)addFriend:(Person *)aFriend;
- (void)removeFriend:(Person *)aFriend;
- (void)sayHelloToFriends;
 
@end
复制代码

咱们在扩展的类名后面的括号中包含了categories名称,而不是正常的@interface声明。 categories名称能够是任何名称,只要它不与同一个类的其余categories冲突便可。 categories的文件名应该是类名,后跟加号,后跟categories的名称(例如,Person + Relations.h)。工具

因此,这定义了咱们categories的接口。 咱们在这里添加的任何方法都将在运行时添加到原始的Person类中。 例如addFriendremoveFriendsayHelloToFriends方法都在Person.h中定义,但咱们能够保持咱们的功能封装和可维护。 另请注意,您必须导入原始类Person.h的标头。 categories实现遵循相似的模式:spa

// Person+Relations.m
#import "Person+Relations.h"
 
@implementation Person (Relations)
 
- (void)addFriend:(Person *)aFriend {
    [[self friends] addObject:aFriend];
}
 
- (void)removeFriend:(Person *)aFriend {
    [[self friends] removeObject:aFriend];
}
 
- (void)sayHelloToFriends {
    for (Person *friend in [self friends]) {
        NSLog(@"Hello there, %@!", [friend name]);
    }
}
 
@end
复制代码

上述代码实现了Person + Relations.h中的全部方法。 就像categories的接口文件同样,categories名称出如今类名后面的括号中。 实现中的categories名称应与接口文件中的categories名称匹配。设计

另请注意,没法在categories中定义其余属性或实例变量。 categories必须使用存储在主类中的数据(在此实例中为Friend)。3d

也能够经过简单地从新定义Person + Relations.m中的方法来覆盖Person.m中包含的实现。 这能够用来修补现有的类; 可是,若是您有问题的替代解决方案,则不建议使用,由于没法覆盖categories中定义的实现。 也就是说,与类层次结构不一样,categories是一个扁平的组织结构 - 若是在两个单独的categories中实现相同的方法,则运行时没法肯定使用哪一个categories。

要使用categories,您必须进行的惟一更改是导入categories的头文件。 正如您在下面的示例中所看到的,Person类能够访问Person.h中定义的方法以及Person + Relations.h类别中定义的方法:

// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Person+Relations.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *joe = [[Person alloc] init];
        joe.name = @"Joe";
        Person *bill = [[Person alloc] init];
        bill.name = @"Bill";
        Person *mary = [[Person alloc] init];
        mary.name = @"Mary";
 
        [joe sayHello];
        [joe addFriend:bill];
        [joe addFriend:mary];
        [joe sayHelloToFriends];
    }
    return 0;
}
复制代码

这就是在Objective-C中建立categories的所有内容。

Protected Methods

重申一下,全部Objective-C方法都是public的,没有语法能够将它们标记为private或protected。Objective-C程序能够将categories与.h/.m的范式结合起来,而不是使用所谓真正的protected方法,以实现相同的结果。

这个想法很简单:将“protected”方法声明为单独头文件中的categories。 这使得子类可以“选择加入”protected的方法,而不相关的类像往常同样使用“public”头文件。 例如,采用标准的Ship接口:

// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
- (void)shoot;
 
@end
复制代码

正如咱们屡次看到的那样,这定义了一种名为shoot的公共方法。 要声明受保护的方法,咱们须要在专用头文件中建立Ship categories:

// Ship_Protected.h
#import <Foundation/Foundation.h>
 
@interface Ship(Protected)
 
- (void)prepareToShoot;
 
@end
复制代码

任何须要访问受保护方法的类(即父类和任何子类)均可以简单地导入Ship_Protected.h。 例如,Ship实现应该为受保护的方法定义默认实现:

// Ship.m
#import "Ship.h"
#import "Ship_Protected.h"
 
@implementation Ship {
    BOOL _gunIsReady;
}
 
- (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@"Firing!");
}
 
- (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@"Preparing the main weapon...");
}
@end
复制代码

请注意,若是咱们没有导入Ship_Protected.h,则此prepareToShoot实现将是一个私有方法。 若是没有导入Ship_Protected.h,子类将没法访问此方法。 让咱们将Ship子类化,看看它是如何工做的。 咱们称之为ResearchShip:

// ResearchShip.h
#import "Ship.h"
 
@interface ResearchShip : Ship
 
- (void)extendTelescope;
 
@end
复制代码

这是一个普通的子类接口 - 它不该该导入Ship_Protected.h,由于这会使受保护的方法对任何导入ResearchShip.h的人均可用,这正是咱们试图避免的。 最后,子类的实现导入受保护的方法,并(可选)覆盖它们:

// ResearchShip.m
#import "ResearchShip.h"
#import "Ship_Protected.h"
 
@implementation ResearchShip
 
- (void)extendTelescope {
    NSLog(@"Extending the telescope");
}
 
// Override protected method
- (void)prepareToShoot {
    NSLog(@"Oh shoot! We need to find some weapons!");
}
 
@end
复制代码

要在Ship_Protected.h中强制执行方法的受保护状态,不容许其余类导入它。 他们只会导入超类和子类的普通“公共”接口:

// main.m
#import <Foundation/Foundation.h>
#import "Ship.h"
#import "ResearchShip.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Ship *genericShip = [[Ship alloc] init];
        [genericShip shoot];
 
        Ship *discoveryOne = [[ResearchShip alloc] init];
        [discoveryOne shoot];
 
    }
    return 0;
}
复制代码

因为main.m,Ship.h和ResearchShip.h都没有导入受保护的方法,所以该代码没法访问它们。 尝试添加[discoveryOne prepareToShoot]方法 - 它会抛出编译器错误,由于找不到prepareToShoot声明。

总而言之,能够经过将受保护的方法放在专用的头文件中并将该头文件导入须要访问受保护方法的实现文件来模拟受保护的方法。 没有其余文件应该导入受保护的header。

虽然此处介绍的工做流程是一个彻底有效的组织工具,但请记住,Objective-C从未打算支持受保护的方法。 能够将其视为构建Objective-C方法的替代方法,而不是直接替换C# / Simula样式的受保护方法。 寻找构建类的另外一种方法一般更好,而不是强迫Objective-C代码像C#程序同样运行。

说明

category的一个最大问题是您没法可靠地覆盖同一类的categories中定义的方法。 例如,若是您在Person(Relations)中定义了一个addFriend:方法,后来决定经过Person(Security)类别更改addFriend: 实现,那么运行时没法知道它应该使用哪一种方法,由于categories 根据定义是一个扁平的组织结构。 对于这些状况,您须要恢复到传统的子类化范例。

此外,重要的是要注意categories不能添加实例变量。 这意味着您没法在categories中声明新属性,由于它们只能在主实现中合成。 此外,尽管categories在技术上确实能够访问其类的实例变量,但最好经过其公共接口访问它们,以保护categories免受主实现文件中的潜在更改。

Extensions

Extensions(也称为class extensions)是一种特殊类型的类,它要求在关联类的主实现块中定义它们的方法,而不是在category中定义的实现。 这能够用于覆盖公开声明的属性属性。 例如,有时能够方便地将只读属性更改成类实现中的读写属性。 考虑Ship类的普通接口:

// Ship.h
#import <Foundation/Foundation.h>
#import "Person.h"
 
@interface Ship : NSObject
 
@property (strong, readonly) Person *captain;
 
- (id)initWithCaptain:(Person *)captain;
 
@end
复制代码

类扩展能够覆盖class中的@property定义。 这使您有机会在实现文件中将该属性从新声明为readwrite。 从语法上讲,扩展看起来像一个空的category声明:

// Ship.m
#import "Ship.h"
 
 
// The class extension.
@interface Ship()
 
@property (strong, readwrite) Person *captain;
 
@end
 
 
// The standard implementation.
@implementation Ship
 
@synthesize captain = _captain;
 
- (id)initWithCaptain:(Person *)captain {
    self = [super init];
    if (self) {
        // This WILL work because of the extension.
        [self setCaptain:captain];
    }
    return self;
}
 
@end
复制代码

注意@interface指令后附加到类名的()。 这是将其标记为扩展而不是普通接口或category的缘由。 必须在类的主实现块中声明扩展中出现的任何属性或方法。 在这种状况下,咱们不会添加任何新字段 - 咱们会覆盖现有字段。 可是与category不一样,扩展能够向类中添加额外的实例变量,这就是为何咱们可以在类扩展中声明属性而不是category。

由于咱们使用readwrite属性从新声明了captain属性,因此initWithCaptain:方法能够在自身上使用setCaptain:accessor。 若是要删除扩展名,属性将返回其只读状态,编译器会报错。 使用Ship类的客户端不该该导入实现文件,所以captain属性将保持只读。

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Ship.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Person *heywood = [[Person alloc] init];
        heywood.name = @"Heywood";
        Ship *discoveryOne = [[Ship alloc] initWithCaptain:heywood];
        NSLog(@"%@", [discoveryOne captain].name);
 
        Person *dave = [[Person alloc] init];
        dave.name = @"Dave";
        // This will NOT work because the property is still read-only.
        [discoveryOne setCaptain:dave];
 
    }
    return 0;
}
复制代码

Private Methods

扩展的另外一个常见用例是声明私有方法。 在上一章中,咱们看到了如何经过在实现文件中的任何位置添加私有方法来声明私有方法。 可是,在Xcode 4.3以前,状况并不是如此。 建立私有方法的规范方法是使用类扩展来向前声明它。 让咱们经过稍微改变前一个示例中的Ship的头文件来看一下这个:

// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
- (void)shoot;
 
@end
复制代码

接下来,咱们将从新建立讨论私有方法时使用的示例。 咱们须要在类扩展中向前声明它,而不是简单地将私有prepareToShoot方法添加到实现中。

// Ship.m
#import "Ship.h"
 
// The class extension.
@interface Ship()
 
- (void)prepareToShoot;
 
@end
 
// The rest of the implementation.
@implementation Ship {
    BOOL _gunIsReady;
}
 
- (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@"Firing!");
}
 
- (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@"Preparing the main weapon...");
}
 
@end
复制代码

编译器确保扩展方法在主实现块中实现,这就是它做为forward-declaration的缘由。 然而,由于扩展被封装在实现文件中,因此其余对象不该该知道它,为咱们提供了另外一种模拟私有方法的方法。 虽然较新的编译器能够为您节省这些麻烦,但了解类扩展的工做原理仍然很重要,由于它是直到最近才开始利用Objective-C程序中的私有方法的经常使用方法。

总结

本章介绍了Objective-C编程语言中两个更独特的概念:category和extension。 category是扩展示有类的API的一种方式,extension是一种在主接口文件的API以外添加所需方法的方法。 这两个最初都是为了减轻维护大型代码库的负担而设计的。

相关文章
相关标签/搜索