方针应当有清晰界说的使命, 比方作为特定信息的模型/展现视觉内容/操控信息流等等.

如之前的文章中说到的那样, 类接口界说了方针怎么与外界的交互, 并帮助外界完成那些使命.

有些时分, 咱们或许想要为一些已有的类在特定情况下增加/扩展一些行为. 比方, 咱们或许常常需求在用户界面上显现一个字符串. 相比每次都去创立一个制作字符串的方针而言, 假如或许为NSString类供给一个把自己制作到屏幕上的功用就更好了.

在这种情况下, 在原有类的接口上增加通用功用不是一个最佳的挑选. 制作才能关于字符串方针来说大多数情况下不会用到, 并且关于NSString类来说, 由于它是一个framework中的类, 咱们不能修正它原始的接口和完成.

承继已有的类也不是一个好的解决方案 — 咱们或许希望不只仅在NSString类上, 而是在一切的NSString类的子类上都能够具有这个功用(比方NSMutableString). 并且, 尽管NSString在OS X和iOS 上都是可用的, 可是在不同的渠道上, 制作的代码或许会不同, 那样就需求在每一个渠道都运用一个不同的子类.

因此, Objective-C 供给了分类(Category)扩展(Extension) 的办法, 答应咱们在已有类上增加咱们定制的办法.

分类(Category)

假如咱们需求向已有的类增加办法, 使其在咱们的运用内能够愈加便捷地增加一些功用, 最简单的办法便是运用分类(Category).

声明一个分类的语法和声明类接口的语法很像, 都运用了@interface 关键字, 可是和类接口不同的是, 声明分类时不会指定承继关系, 而是经过括号()将分类称号包起来.

@interface ClassName (CategoryName)
@end

咱们能够为任何类声明分类, 即使咱们没有这个类的完成源码(比方Cocoa 或许 Cocoa Touch 类). 任何咱们在分类中声明的办法, 在对应的类及其子类中都是能够运用的. 在运转时, 在原有类中完成的办法和在分类中完成的办法是没有什么区别的.

拿之前文章中的XYZPerson类举例, 咱们或许频繁地需求展现人名列表:

Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate

咱们能够经过分类来完成这个需求:

#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end

在这个例子中, XYZPersonNameDisplayAdditions 分类声明一个返回字符串的新办法 lastNameFirstNameString

通常分类的声明会放在独自的头文件中, 完成的代码也会放置在独自的源码文件中. 在XYZPerson的例子中, 咱们或许会把上面的分类声明放置在名为XYZPerson+XYZPersonNameDisplayAddtions.h的头文件中.

尽管从分类中增加的办法关于它一切的实例以及子类实例都可见, 可是咱们在运用这些办法时, 仍然是需求引进对应头文件的, 不然就会在编译时呈现正告和过错.

分类的完成如下:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end

一旦咱们完成了分类的声明和完成, 咱们就能够在其它类中运用这些办法了:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
    XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
                                                    lastName:@"Doe"];
    XYZShoutingPerson *shoutingPerson =
                        [[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
                                                            lastName:@"Robinson"];
    NSLog(@"The two people are %@ and %@",
         [person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end

分类不只仅能够为咱们供给为已有类增加办法的功用, 咱们也能够运用分类将一个复杂的类的完成分开成多个文件办理. 比方当咱们要自界说一个UI元素时, 假如几许核算, 色彩, 渐变等特别复杂时, 咱们就能够把制作相关的代码抽离出来, 由独自的分类办理. 咱们也能够根据不同的渠道(OS X/iOS)创立不同的分类以供给不同的完成办法.

分类能够用来声明实例办法或许类办法, 可是大多数情况下不适宜声明额定的特点. 尽管从语法角度看在分类的声明中声明特点是可行的, 可是却不能够在分类中声明额定的实例变量. 这就意味着编译器不会为咱们自动组成任何实例变量, 也不会组成任何特点的拜访办法(setter/getter). 咱们尽管能够在分类完成中自己完成getter/setter办法, 可是咱们没有办法存储特点对应的实例变量, 除非是原有类中已有的实例变量.

仅有的向已有类增加特点且能够生成对应实例变量的办法便是运用后文行将解说的类扩展(Extension).

Note: Cocoa 和 Cocoa Touch 中包含了大量的为结构中的已有的类增加的分类.

在本文介绍中提及的为NSString类供给的制作功用在OS X中是经过NSStringDrawing分类完成的, 在其中包含了drawAtPoint:withAttributes:drawInRect:withAttributes: 办法. 在iOS中是经过UIStringDrawing分类完成的, 包含drawAtPoint:withFont:drawInRect:withFont: 等办法.

防止分类办法名抵触

由于在分类中声明的办法是向已有的类中增加的, 所以关于办法称号咱们需求分外当心.

假如在分类中声明的办法称号和原有类中的办法称号一致,或许和其它分类中的办法一致(甚至是父类中的办法称号), 在运转时调用此办法的行为是不确定的. 让咱们为自己的类编写分类时, 呈现这种问题的或许性不大, 可是假如是在为Cocoa 或许 Cocoa Touch 增加分类, 就或许引发问题.

假定咱们在编写一个需求和远程web服务交互的运用, 咱们就或许需求常常对字符串进行Base64编码. 因此咱们或许会为NSString界说一个分类, 增加一个返回 Base64 编码后的字符串办法, 因此咱们或许增加一个叫做base64EncodedString的分类办法.

假如咱们又链接了另外一个framework, 而这个framework 恰巧也界说了一个叫做base64EncodedString的办法, 那么问题就呈现了: 在运转时调用base64EncodedString办法时, 这两个办法只有一个会被调用, 而至于哪一个最终会被调用, 便是不确定的了.

还有另外一种情况或许发生问题: 假如咱们为Cocoa/Cocoa Touch类增加了分类办法, 而后Cocoa/Cocoa Touch 类在后续更新中在原类中增加了此办法. 比方NSSortDescriptor类, 它的功用是描绘集合方针应当怎么排序, 一直以来就具有initWithKey:ascending:的初始化办法. 可是在前期OS X 和 iOS 版别并没有供给对应的类工厂办法.

依照习气, 这个类工厂办法应当叫做sortDescriptorWithKey:ascending:, 因此咱们或许现已增加了NSSortDescriptor的分类, 供给了这个工厂办法. 这样, 在前期的OS X或许iOS版别上, 代码是能够依照咱们的预期运转的, 可是在Mac OS X 10.6 和 iOS 4.0 之后NSSortDescriptor类就增加了sortDescriptorWithKey:ascending:办法, 此刻咱们分类中的办法和体系类的办法就发生了办法称号的抵触.

为了防止这种问题, 在为framework中的类增加分类时最好在办法称号前增加前缀, 正如咱们命名咱们自界说的类时一样. 咱们能够运用和类前缀相同的三个字母, 将其转为小写后与办法自身的称号以下划线_衔接, 以遵循办法称号命名规则. 关于前面的NSSortDescriptor为例, 咱们为其界说的分类或许是如下办法的:

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

这就意味着咱们能够坚信咱们的代码会依照预期在运转时被调用. 由于咱们的代码办法类似如下, 原有的歧义也因此被消除:

NSSortDescriptor *descriptor =
               [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];

类扩展(Extension)

类扩展和分类有些类似, 可是咱们只能在编译时为咱们已有源码的类增加类扩展(类和类扩展在编译时一起被编译). 在类扩展中声明的办法是在原有类的@implementation块中被完成的. 因此咱们无法为一个framework中的类(比方Cocoa/Cocoa Touch 中的类, 像是NSString)增加类扩展.

声明类扩展的语法和分类很类似:

@interface ClassName ()
@end

由于在括号中没有称号, 类扩展又常常被称为匿名分类(anonymous category)

和一般分类不同的是, 类扩展能够为类增加特点和实例变量. 假如咱们在一个类扩展中像这样增加一个特点:

@interface XYZPerson ()
@property NSObject *extraProperty;
@end

编译器会自动在类的完成中组成相关的拜访办法(setter/getter)以及实例变量.

假如咱们在类扩展中增加了办法, 那么这些办法必须在类的完成中完成.

咱们也能够在类扩展的{}中增加自界说的实例变量 :

@interface XYZPerson () {
    id _someCustomInstanceVariable;
}
...
@end

运用类扩展隐藏私有信息

类的首要接口用来界说怎么和外界交互, 也便是说它是类的公共接口(public interface).

类扩展常常运用一些额定的私有办法和特点以更好地为扩展公共接口. 比方在类接口中界说一个特点为readonly, 可是在类扩展中将其界说为readwrite, 这样既能在类内部直接修正特点值,对外又能够操控特点的写入权限.

比方XYZPerson类或许增加了一个叫做uniqueIdentifier的特点, 用来存储身份证号码.

uniqueIdentifier对外只读, 并且类接口供给了一个分配uniqueIdentifier的办法.

@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end

这就意味着其它无法直接设置uniqueIdentifier. 假如需求从头分配uniqueIdentifier,需求调用assignUniqueIdentifier办法.

为了让XYZPerson内部具有修正特点的值的才能, 咱们能够在完成文件上方增加类扩展从头界说uniqueIdentifier特点:

@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end

Note: readwrite attribute 是可选的, 由于它是默认的attribute, 当从头界说一个特点的时分能够显式声明以达到着重弄清的效果.

这样的话, 编译器也会自动组成一个setter办法, 因此XYZPerson类的完成就能够直接运用setter办法或许点语法来设置特点值了.

XYZPerson类的完成文件中增加类扩展, 类扩展中的信息关于XYZPerson类是私有的. 假如其它方针测验设置特点, 编译器将发生过错.

Note: 上述例子经过增加类实例, 从头声明了uniqueIdentifier并将其设置为readwrite . 无论其它源文件是否知道它是类扩展中从头声明的, 它的setter办法: setUniqueIdentifier: 办法在运转时关于每个方针都是存在的.

当测验在其它源文件中调用私有办法或许测验为readonly修饰的特点赋值时, 编译器会发生过错, 可是能够经过动态运转时调用这些办法来规避编译器报错, 比方运用NSObject类供给的performSeletor:...方法. 当必要的时分, 咱们能够经过这种办法绕开类的约束, 可是公共类接口应该总是正确地界说对外的行为.

假如咱们想要让”私有”办法或许特点对单个类可见时, 比方同framework中的相关类. 咱们能够在一个新的头文件中声明类扩展, 并在需求运用它的类中导入头文件. 同一个类有两个头文件的情况也时有发生, 比方能够界说XYZPerson.hXYZPersonPrivate.h. 可是当咱们发布framework时, 咱们只应当对外开放XYZPerson.h头文件.

运用其它办法为已有类增加功用

分类和扩展使得为已有的类增加功用十分方便, 可是有时分他们不是最好的解决方案.

面向方针言语的首要方针之一便是写出可复用的代码, 也便是使类在尽或许多的情况下能够复用.假如咱们正在创立一个视图类以描绘一个方针并将其信息展现在屏幕上, 咱们应当多多考虑这个类是否能够在多种情况下复用.

为了防止硬编码布局和UI内容的代码, 其中一个办法便是运用承继, 让子类重写一些办法, 把那些抉择留到子类去做. 尽管这样能够使得类能够轻松地被复用, 可是在咱们每次想要运用这个类时都需求创立一个新的子类.

另外一个办法便是为类创立一个代理方针. 一切或许约束复用性的抉择都由代理方针处理, 这样抉择就推延到了运转时. 一个常见的例子便是TableView和TableView的代理. 为了提高TableView的复用性, TableView把关于它内容的抉择都交给了另外一个方针在运转时抉择. 代理会在后面的文章中详细讲述, 详细能够参考官方文档: Working with Protocols.

直接经过Objective-C运转时增加

Objective-C 经过它的 Objective-C 运转时体系供给了动态的行为.

很多抉择 — 比方当消息发送出去之后, 要调用那个办法 — 在编译时是不确定的, 可是当程序运转起来之后, 会在运转时抉择. Objective-C 不只是一个编译成机器码的言语, 它供给了一个运转时体系去动态履行那些机器码.

咱们能够经过直接与运转时体系交互, 比方给方针增加相关引证(associative reference). 和类扩展不同的是, 相关引证并不影响原有类的声明和完成, 这就意味着咱们能够运用相关引证修正咱们没有权限拜访的源码(比方framework中的类).

相关引证把一个方针和另一个方针相关起来, 和特点或实例变量的完成办法很类似. 更多关于相关引证请参考官方文档Associative References. 更多关于运转时的信息, 请参考官方文档Objective-C Runtime Programming Guide. (之后这部分文档也会连续翻译出来的.)

练习

  1. XYZPerson类增加一个分类, 增加一些额定的办法, 比方以不同办法展现人名.
  2. NSString增加一个分类, 在分类中增加一个办法以完成在某一个点出制作该方针代表的全大写字符串. 能够经过调用NSStringDrawing分类中的办法来完成实际的制作.
  3. 为原有的XYZPerson类增加两个只读特点分别代表这个人的身高和体重, 并增加measureWeightmeasureHeight办法. 运用类扩展并且从头声明这些特点为可读写, 并完成上述办法为特点设置适宜的值.

参考资料: Customizing Existing Classes