Categories - 分类

来源于 Ry’s Objective-C Tutorial - RyPress

一个学习Objective-C基础知识的网站.

个人觉得很棒,所以决定抽时间把章节翻译一下.

本人的英语水平有限,有让大家误解或者迷惑的地方还请指正.

原文地址:http://rypress.com/tutorials/objective-c/categories.html

仅供学习,如有转摘,请注明出处.


分类是一个能让类的定义分到多个文件中.这样做的目的是为了减轻(将类模块化时需要)维护大量代码的压力.从而避免你的源代码变成一个有10000+代码行的文件,因为这样的文件肯定很难操控(导航),而且对独立的开发人员来说,如果想定义一个类中的一些特殊的,良好定义的部分也是很困难的.

Using multiple files to implement a class
Using multiple files to implement a class

在这个模块,我们将会在不碰源文件的情况下,使用分类来对已有的类进行扩展.同样地,我们也将看到这样的功能是怎样被用于实现(模拟)受保护方法的.Extensions跟分类关系很大(很类似),我们也会简单的学一下.

Setting Up

在我们开始实践分类之前,我们需要一个有效的基础类.创建一个或者修改已存在Car类接口,如下:

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property (copy) NSString *model;
@property (readonly) double odometer;

- (void)startEngine;
- (void)drive;
- (void)turnLeft;
- (void)turnRight;

@end

对应的实现文件仅仅输出一些描述信息,以便我们知道是在调用不同的方法.

// Car.m
#import "Car.h"

@implementation Car

@synthesize model = _model;

- (void)startEngine {
    NSLog(@"Starting the %@'s engine", _model);
}
- (void)drive {
    NSLog(@"The %@ is now driving", _model);
}
- (void)turnLeft {
    NSLog(@"The %@ is turning left", _model);
}
- (void)turnRight {
    NSLog(@"The %@ is turning right", _model);
}

@end

现在,假设你想增加另一组有关Car维修的方法.那么你可以在一个专用的分类中添加这些新方法,而不是继续往原有的Car.h与Car.m里面塞.

创建分类

略.
分类命名的唯一要求就是,当基于同一个类创建分类时,不能与其他分类名冲突(相同).公认的文件命名规约是使用加号隔开类名与分类名,所以你在保存上述创建的分类后,你会在Xcode's 的项目导航看到一个Car+Maintenance.h和一个Car+Maintenance.m文件.

正如你在Car+Maintenance.h中所见,一个分类的接口看起来完全像一个普通类接口,除了在类名之后跟了一个带括号的分类名.(现在),让我们继续给分类添加一些方法.

// Car+Maintenance.m
#import "Car+Maintenance.h"

@implementation Car (Maintenance)

- (BOOL)needsOilChange {
    return YES;
}
- (void)changeOil {
    NSLog(@"Changing oil for the %@", [self model]);
}
- (void)rotateTires {
    NSLog(@"Rotating tires for the %@", [self model]);
}
- (void)jumpBatteryUsingCar:(Car *)anotherCar {
    NSLog(@"Jumped the %@ with a %@", [self model], [anotherCar model]);
}

@end

在运行时,这些方法则会成为Car类的一部分.尽管它们被声明在一个不同的文件中,但你仍可以访问它们,好像它们(这些方法)就是在Car.h原始文件中声明地.

当然,你必对给这些分类接口中方法进行实现,以便它们能处理事情.分类的实现跟一个标准的类实现基本上一样,除了分类的名称是放在类名之后的括号内的.

// Car+Maintenance.m
#import "Car+Maintenance.h"

@implementation Car (Maintenance)

- (BOOL)needsOilChange {
    return YES;
}
- (void)changeOil {
    NSLog(@"Changing oil for the %@", [self model]);
}
- (void)rotateTires {
    NSLog(@"Rotating tires for the %@", [self model]);
}
- (void)jumpBatteryUsingCar:(Car *)anotherCar {
    NSLog(@"Jumped the %@ with a %@", [self model], [anotherCar model]);
}

@end

请注意很重要的一点,一个分类也可以被用来重写在基类中的方法(比如,Car类中的drive方法),但你永远都不应该这么做.问题在于分类是一个平行的结构.如果你在Car+Maintenance.m重写了已存在的方法,随后你却要在另一个分类中变更它(已存在的这个方法)的行为,这对OC来说,无法识别该使用哪个实现.这种情况下,使用子类往往是更好的选择.

使用分类

任何需要使用一个分类中定义的API的文件都需要导入分类的头文件,这与一个普通类的使用相同.在你导入Car+Maintenance.h后,它所有的方法都能直接通过Car类使用.

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Car+Maintenance.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *porsche = [[Car alloc] init];
        porsche.model = @"Porsche 911 Turbo";
        Car *ford = [[Car alloc] init];
        ford.model = @"Ford F-150";
        
        // "Standard" functionality from Car.h
        [porsche startEngine];
        [porsche drive];
        [porsche turnLeft];
        [porsche turnRight];
        
        // Additional methods from Car+Maintenance.h
        if ([porsche needsOilChange]) {
            [porsche changeOil];
        }
        [porsche rotateTires];
        [porsche jumpBatteryUsingCar:ford];
    }
    return 0;
}

如果你移除了Car+Maintenance.h这个导入语句,那么Car类将恢复到它的原始状态,编译器也会抱怨说,来自Maintenance分类的needsOilChange,changeOil以及其他方法都不存在.

"受保护"方法

分类不仅仅用来将类的定义分在几个文件而已.它们也是一个强大的组织(代码的)工具.(这个工具)通过导入分类来将一个API的指定的一部分(方法)成为任意类中的方法,而对其他的类文件来说(除了当前的这个这个导入分类的文件),这些API仍保持隐藏(状态).

回忆一下Methods模块中描述的,OC中是不存在受保护的方法的.然而,可以通过分类的导入来实现与受保护访问修饰符等同的效果.就是在一个专用的分类中定义一个受保护的API,然后只把它导入它的子类实现中.这就使得这个受保护的方法对子类有效(可用),而保持对程序中其他部分的隐藏.比如:

// Car+Protected.h
#import "Car.h"

@interface Car (Protected)

- (void)prepareToDrive;

@end
// Car+Protected.m
#import "Car+Protected.h"

@implementation Car (Protected)

- (void)prepareToDrive {
    NSLog(@"Doing some internal work to get the %@ ready to drive",
          [self model]);
}

@end

上述这个Protected分类定义了一个让Car以及它子类内部使用的方法.为了看到这种情景,我们修改Car.m的drive方法,让它使用这个受保护的prepareToDrive方法:

// Car.m
#import "Car.h"
#import "Car+Protected.h"

@implementation Car
...
- (void)drive {
    [self prepareToDrive];
    NSLog(@"The %@ is now driving", _model);
}
...

接下来,创建一个Car的子类 - Coupe,来看一下受保护方法是怎样工作的(实现的).在(Coupe)接口中没任何特别,但注意类实现是怎样通过导入Car+Protected.h来指定(使用)这个受保护API的.如果你想,你也可以在Coupe.m中重新定义这个方法来重写这个受保护的方法.

// Coupe.h
#import "Car.h"

@interface Coupe : Car
// Extra methods defined by the Coupe subclass
@end
// Coupe.m
#import "Coupe.h"
#import "Car+Protected.h"

@implementation Coupe

- (void)startEngine {
    [super startEngine];
    // Call the protected method here instead of in `drive`
    [self prepareToDrive];
}

- (void)drive {
    NSLog(@"VROOOOOOM!");
}

@end

为了迫使Car+Protected.h中方法的受保护状态,即只对其子类有效,那么千万别把这个分类导入到其他文件中.在下面的main.m中,你会看到受保护的prepareToDrive方法通过[ford drive]以及[porsche startEngine]调用,如果你尝试直接调用,那编译就不愿意了(会抱怨).

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Coupe.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *ford = [[Car alloc] init];
        ford.model = @"Ford F-150";
        [ford startEngine];
        [ford drive]; // Calls the protected method
        
        Car *porsche = [[Coupe alloc] init];
        porsche.model = @"Porsche 911 Turbo";
        [porsche startEngine]; // Calls the protected method
        [porsche drive];
        
        // "Protected" methods have not been imported,
        // so this will *not* work
        // [porsche prepareToDrive];
        
        SEL protectedMethod = @selector(prepareToDrive);
        if ([porsche respondsToSelector:protectedMethod]) {
            // This *will* work
            [porsche performSelector:protectedMethod];
        }
        
        
    }
    return 0;
}

当然,你可以通过performSelector:来动态的访问到prepareToDrive:.再强调一次,OC中的所有方法都是公有的,也没有方式真正实现将这些方法对客户端代码(client code,直译的)隐藏.分类仅仅是一个基于规约来控制API中哪些部分应该对其他文件可用的方式.

扩展

扩展与分类相似,都能让你在一个类的接口(头文件)外部添加方法.而与分类的区别在于,扩展添加的方法必须在对应的主实现文件中实现-而不能在分类中实现.

可以通过将方法添加到实现文件中,不是接口文件,来模拟私有方法.这对私有方法较少的情况很有效,但对更大的类来说,(就种实现的类)会变得很难控制(臃肿).扩展则通过允许声明正式的私有API来解决这个问题.

举个例子,如果你想在上述定义的Car类中正式的添加一个私有方法-engineIsWorking,你可以在Car.m中包含一个扩展.但由于它被声明在Car.m中,而不是Car.h接口文件中,所以如果在@implementation中没有被定义,那么编译器就会找你麻烦.扩展的语法像一个空(括号中没有名称)的分类:

// Car.m
#import "Car.h"

// The class extension
@interface Car ()
- (BOOL)engineIsWorking;
@end

// The main implementation
@implementation Car

@synthesize model = _model;

- (BOOL)engineIsWorking {
    // In the real world, this would probably return a useful value
    return YES;
}
- (void)startEngine {
    if ([self engineIsWorking]) {
        NSLog(@"Starting the %@'s engine", _model);
    }
}
...
@end

除了可以声明正式的私有API之外,扩展也可以被用来重新声明公有接口中的属性.这种方式经常被用来设置属性在类内部可读写,而对其他的对象保持只读.下面的例子中,如果我们变更上述类的扩展:

// Car.m
#import "Car.h"

@interface Car ()
@property (readwrite) double odometer;
- (BOOL)engineIsWorking;
@end
...

我们可以在内部通过self.odometer来(给odometer)分配值,但是如果在Car.m之外尝试这么做就会有编译错误.

重新将属性设置为可读写以及创建正式的私有API对小点的类并不是很有用.对于那些你需要组织大的框架来说才是它们真正发挥效用的地方.

在Xcode4.3之前,因为方法在被使用之前必须先声明的原因,所以扩展很常见.这对很多开发者来说都不爽,同时,因为扩展扮演着私有方法的向前声明角色,所以,即使在自己的项目中不使用上述的那种方式,你也会在你搞OC开发的职业生涯中遇到.

总结

这个模块涵盖了OC的分类和扩展.分类是通过将实现文件分割来模块化类的一种方式.扩展则是提供(跟分类)相似的功能,不同在于,它的API必须在对应的主实现中声明.

除了组织大的代码库之外,分类的其中一个常用方式是对内嵌的NSString或者NSArray这种数据类型添加方法.这种优势在于,你不必使用一个新的子类来更新存在的代码,但注意,你千万小心-不要去重写已经存在的功能.对稍小的个人项目来说,(创建)分类没有太多的必要(都不值得麻烦的去创建一个),而使用子类和协议这些规范则会让你省去一些让你头疼的调试烦恼.

在下个模块,我们将探讨另一个被称作的(代码)组织工具.块是用来代表和传递任意语句的方式.(这种方式)对编程范例开启了一个全新的世界.


写于09月09号,完成与09月11号

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,081评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • 我跟jj说,恰好年轻是最好的年纪 其实我错了,年轻不一定最好 辛苦却不疲惫,才是最好的年纪 这激扬的岁月,我的脚步...
    喜欢你春风一样的优雅阅读 331评论 0 1
  • 如果谁说自己的缺点是懒,那他是把一百件事当成一件说了。挺会遮丑——这也是懒人的特长。 懒虽然只有一个字,但是会衍生...
    杨一同学阅读 341评论 2 3
  • 我自己把所谓的世界经典文学作品分为三类: -1- 望而生畏型 这方面的代表作如《战争与和平》、《追忆逝水年华》等等...
    清澈的北风阅读 6,058评论 152 227