每一个对象都需要有明确的任务,例如建立信息模型、可视化展示或者信息流程的控制,一个类的interface就定义了可以对外交互的信息,通过这些信息完成特定的任务。但有时,你可能需要对已经存在的类做一定的功能扩展,用于某些特别的情况。
例如,如果你的应用程序需要经常可视化展示字符串,相对于每次展示都去调用绘制字符串的对象,如果能直接给NSString类添加一个绘制自身字符串的功能会更好一些。这种情况下,直接修改类的原始类借口并不是很好的方法,因为在大多数用到字符串的应用中,绘制字符串的功能并不会用到,而且对于NSString, 它更是官方提供的框架类,你并不能够去修改它原始的interface和实现。
更进一步,直接使用继承也并不可行,因为你可能不仅希望NSString类有这种功能,还希望它的子类如NSMutableString也有这种功能。并且,尽管NSString 在OS X 和iOS都可以使用,但不同平台下,还是需要实现不同的子类。
Objective-c 允许你对已存的类通过分类和扩展来增加你自己的方法。
(一) 通过分类增加已存在类的方法
如果你需要对已知的类增加一个方法,特别是在应用中增加一个功能使得做其他的事更简单,最简单的方法就是使用分类。
分类的语法如下所示:
@interface ClassName (CategoryName)
@end
首先,使用@interface关键字来定义分类,就像定义一个objective-c类一样,不同的是,它并不从其他类继承,只是多了个括号,并在括号中指定分类的名称。
可以为任意的类定义分类,即使你并没有这个类的源码(比如标准的Cocoa 或者 Cocoa Touch 类),你在分类中定义的任何方法都可以被它的类及子类的对象使用。在runtime,分类定义的方法和原始类定义的方法是没有区别的。
假如你已经定义了一个XYZPerson类,在一个应用中,你需要频繁的使用类的lastname,如:
Appleseed, John |
Doe, Jane |
Smith, Bob |
Warwick, Kate |
<pre name="code" class="objc">#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end
分类经常在一个独立的头文件中定义,并且实现是在独立的源文件中。例如对XYZPerson类,头文件也许是这样定义的:
XYZPerson+XYZPersonNameDisplayAdditions.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
除了对已存在的类添加方法外,分类还可以用来将一个复杂的类划分为多个文件,例如:你可以讲绘图的功能放在一个单独的文件中,其他的放在其他文件。
分类可以用来申明实例方法或者是类方法,但是并不适合定义属性。
虽然语法上来说在分类的interface中增加属性是合法的,但是分类中不可能申明一个额外的实例变量,意味着编译器既不会合成任何的实例变量,也不会合成属性的访问方法,你可以自己实现一个分类的访问器方法,但是并不能实施跟踪属性的值,除非这个值就存储在原先类中。
对一个类增加属性的唯一方法就是使用类扩展
(二)防止分类的方法名冲突
在分类中定义的方法名最后要被添加到已存的类中,所以需要注意方法名。
如果分类中定义的方法名与原先的类方法名同名,或与同一个类的另一个分类(或父类)包含相同的方法名,那么运行时不知道执行哪一个。当使用你自己的类时,可能并不是个大问题,但当问题出现在对标准的Cocoa和Cocoa Touch类添加分类的方法时,这样就会出问题。
所以最好在方法名前添加前缀,例如你的分类可以定义如下:
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
这意味着你的方法现在可以在runtime正确的使用了,因为你现在是这样使用的:
NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];
(三、类扩展延伸了内部实现)
类扩展与分类有些相似,但区别在于类扩展只能添加在已有源代码的类中,类扩展的方法在原始类的@implementation块中实现,因此基础类的Cocoa和Cocoa Touch如NSString并不能添加类扩展。
类扩展和分类的语法是相似的:
@interface ClassName()
@end
因为括号中没有名字,类扩展就像一个匿名分类。
与分类不同,类扩展可以添加自己的类属性和实例变量。如果在类扩展中定义一个属性,如下:
@interface XYZPerson()
@property NSObject *extraProperty;
@end
编译器自动在主类的实现中合成相关的访问器方法,和一个实力变量。
如果类扩展中添加方法,添加的方法必须在主类中实现。
如果类扩展中添加实例变量,需要定义在类的接口中:
@interface XYZPerson(){
id _someCustomInstanceVariable;
}
...
@end
(四)利用类扩展隐藏私有信息
一个类的interface主要用来定义和其他类交互的方法,换句话说,对类是共有的interface。
类扩展一般利用额外的私有方法和属性来拓展公有的interface。比如,可以在interface中定义一个readonly的属性,但是在类扩展中将其定义为readwrite,以便类内部的方法可以直接修改属性值。
例如:XYZPerson类有一个属性值uniqueIdentifier,用来记录个人身份信息。一般来说,XYZPerson的类interface可以申明属性为readonly,并且提供一个分配值的方法,就像这样:
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
这意味着并不能直接通过其他对象直接对uniqueIdentifier进行赋值,必须要通过assignUniqueIdentifier方法。
为了在XYZPerson类内部改变属性值,在类的实现文件中重新定义一个属性是比较明智的。
@interface XYZPerson()
@property (readwrite) NSString *uniqueIdfentifier;
@end
@implementation XYZPerson
...
@end
Note:
The
readwrite
attribute is optional, because it’s the default. You may like to use it when redeclaring a property, for clarity.
这意味着编译器合成了一个setter方法,XYZPerson实现中的任意方法可以通过setter或者点号直接赋值。
在XYZPerson类的implementation文件中实现了类的扩展,这些扩展信息是XYZPerson类私有的,如果其他类对象向设置属性,编译器就会报错。
注意:通过上面增加类扩展,重新定义了uniqueIdentifier属性为readwrite,在每个XYZPerson对象的runtime就有了一个setUniqueIdentifier:方法。
编译器在其他源代码试图访问一个私有方法或者readonly属性的时候会报警,但也可以利用其它方法阻止编译报错,比如利用NSobject类提供的performSelector:...。设计类时,类的interface需要定义正确的公用对外借口。
如果你打算私有的方法或属性能够被某些其他在同一个框架内中的相关类使用,你可以在单独的头文件中定义类的extension,然后导入到其他需要使用的源文件中,一个类有两个头文件并不稀奇,不如,XYZPerson.h和XYZPersonPrivate.h文件,你发布框架的时候,仅仅需要发布公用的XYZPerson.h头文件就可以了。
以上翻译文件来源于IOS developer library:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW1