Customizing Existing Classes
对象应该有明确的任务,如建模的具体信息,显示视觉内容或控制信息的流通。正如你已经看到的,一个类接口定义一些方法在于其他希望交互的对象上,以帮助其完成这些任务。
有时,你可能发现你希望通过添加一些行为来扩展现有的类,但是紧在一些情况下有用。例如,你可能发现你的应用经常需要在视觉界面显示一个字符串。与其创建一些书写字符串的方法来显示你需要的字符串,不如给NSString类添加一个功能用来在屏幕上显示字符串来的更有意义。
像这种情况下,在原始的主类接口中添加一个使用的功能并不总是有意义的。绘图能力在应用程序中大多数的时候不太可能需要任何字符串对象使用,例如,在NSString中,你不能定义或者实现这个功能在原始的接口中,因为她是一个框架类。
此外,仅仅只是把这个功能变成现有类的子类可能没有多少意义,因为你可能希望你的绘图功能的实现不仅是对于原始的NSString类,也能在任何其他的类的子类中实现,比如NSMutableString类。尽管NSString类在OS X和iOS系统中都是可用的,但是绘图代码需要为每个平台实现,所以你需要使用不同的子类在不同的平台上。
相反,OC允许你添加自己的方法为已经存在的类,这会用到 类别 和 类的延展 。
Categories Add Methods to Existing Classes
如果你需要为一个已经存在的类添加一个方法,也许添加一个功能在你自己的应用中以便使她变的更容易,最简单的方法就是使用类别。
声明一个类别的语法需要使用@interface关键字,就像标准的OC类描述,但是不需要表明任何的继承关系,相反的是要在括号中指明类别的名字,像这样:
@interface ClassName (CategoryName) |
|
@end |
任何类都可以声明类别,即使你没有原始的实现代码(比如标准的Cocoa或者Cocoa Touch类)。任何你在类别中声明的方法对于原始类所有的实例都是有效的。在运行时,原始类的方法和通过类别添加的方法是没有区别的。
考虑一下前面章节提到过的XYZPerson类,类的属性是姓名。如果你写一个档案记录应用,你可能会发现你会经常需要显示一个姓氏的名单,想这样:
Appleseed, John |
Doe, Jane |
Smith, Bob |
Warwick, Kate |
Rather than having to write code to generate a suitable lastName, firstName
string each time you wanted to display it, you could add a category to the XYZPerson
class, like this:
#import "XYZPerson.h" |
|
@interface XYZPerson (XYZPersonNameDisplayAdditions) |
- (NSString *)lastNameFirstNameString; |
@end |
本例中,XYZPersonNameDisplayAdditions
类别声明了一个额外的方法用来返回必要的字符串。
类别通常声明在头文件中,实现在源文件中。在XYZPerson类中,你可以在头文件XYZPerson+XYZPersonNameDisplayAdditions.h声明类别。
虽然为任何类和子类的所有实例添加类别都是有效的,在你需要使用额外的方法时,你可能会需要在源文件中包含类别的头文件,否则编译器会有警告和错误。
类别的实现:
#import "XYZPerson+XYZPersonNameDisplayAdditions.h" |
|
@implementation XYZPerson (XYZPersonNameDisplayAdditions) |
- (NSString *)lastNameFirstNameString { |
return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName]; |
} |
@end |
Once you’ve declared a category and implemented the methods, you can use those methods from any instance of the class, as if they were part of the original class interface:
#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 |
你不仅可以在一个已经存在的类中添加方法,而且你可以通过使用类别来分割一个复杂的类,把他们实现在多个源文件当中。例如,你可以把为自定义用户界面绘图的代码放在一个单独的文件里,其他复杂的实现如几何计算,颜色,渐变等放在一个文件中。或者,你可以提供不同的类别实现,选择哪种方法取决于你编写的app是为了OS X还是iOS.
类别可以被用来声明实例方法或者是类方法,但是不适合用来声明额外的属性。在一个类别的声明中包含属性是有效的语法,但是再类别中不可能声明一个额外的实例变量。这意味着编译器不会何处任何的实例变量,也不会合成任何的访问器方法。你可以在类别的实现中自己添加访问器方法,但是你不可能为了属性追踪一个值,除非这个值是被原始的类存储的。
唯一可以添加额外属性的方法--一个实例变量支持的方法--是为已经存在的类使用类的延展,详见Class Extensions Extend the Internal Implementation。
Note: Cocoa and Cocoa Touch include a variety of categories for some of the primary framework classes.
The string-drawing functionality mentioned in the introduction to this chapter is in fact already provided for NSString
by the NSStringDrawing
category for OS X, which includes thedrawAtPoint:withAttributes:
and drawInRect:withAttributes:
methods. For iOS, the UIStringDrawing
category includes methods such as drawAtPoint:withFont:
anddrawInRect:withFont:
.
Avoid Category Method Name Clashes
因为声明在类别中的方法是添加到一个已经存在的类中,你需要小心的就是方法的名字。
如果声明在类别中的方法的名字和原始类中方法的名字相同,或者在同一个类中有另一个类别中的方法已经用了你使用的名字(或者父类中),那么在运行时,那个方法的实现会被调用是不确定的。如果你在你自己的类中使用类别,那么前面的情况不大可能发生,但是当你为标准 的Cocoa或者Cocoa Touch类使用类别添加方法时可能会引起问题。
例如,一个使用远程web服务的应用程序,可能需要使用一个简单的方法来通过64位编码方式为一个字符串进行编码。那么你为NSString类定义一个类别,添加一个实例方法用来返回一个64编码的字符串是很有意义的方法,因此你可以添加一个便利的方法叫做base64EncodedString。
如果你需要连接另一个框架,这个框架碰巧也定义了她自己的类别在NSString中,这个方法也叫base64EncodeString,这时就有了一个问题。在运行时,只有一个方法的实现会“胜出”,然后添加到NSString中,另一个就会变成是未定义的状态。
如果你为Cocoa或者Cocoa Touch类添加一个便利的方法,在以后的版本中,这些方法稍后会添加到原始的类里面,这样也会出现一个问题。例如,NSSortDescriptor类,描述了一个集合的对象如何变得有序,这个类有一个初始化方法initWithKey:ascending,但是在早期的OS X和iOS版本中,她并没有提供一个相应的类工厂方法。
按照惯例,类的工厂方法应该叫做sortDescriptorWithKey:ascending:,因此你也可以选择添加一个类别在NSSortDescription中提供这个便利的方法。这样做在老的版本中同样是有效的,但是在Mac OS X10.6版本和iOS4.0版本中一个sortDescriptionWithKey:ascending方法被添加到原始类NSSortDescription类中,这就意味着,当你的程序运行在这些活着以后的平台的时候,你会以一个命名冲突停止原来的添加类别行为。
为了避免未定义的行为出现,最好为框架类中的类别方法添加一个命名的前缀,就像你应该为你自己的类名添加一个前缀一样。你可以选择使用同样的三个字母,但是按照惯例通常方法名使用小写字母,然后一个下划线,然后是方法名。对于NSSortDescription这个例子来说,你自己的类别应该是这样的:
@interface NSSortDescriptor (XYZAdditions) |
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending; |
@end |
This means you can be sure that your method will be used at runtime. The ambiguity is removed because your code now looks like this:
NSSortDescriptor *descriptor = |
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES]; |
Class Extensions Extend the Internal Implementation
类的延展和类别有些相似,但是类延展只能添加到在编译时你拥有源代码的类中(类是和类延展同时编译的)。通过类延展声明的方法实现在原始类@implementation块中,因此你不能在一个框架类上声明类延展,比如Cocoa或者Cocoa Touch类提供的像NSString。
声明类延展的语法和类别的语法相似,像这样:
@interface ClassName () |
|
@end |
因为类延展声明中的括号内没有提供名字,所以类延展通常被称为匿名类别。
类延展添加自己的属性和实例变量在一个类中通常与类别不同。如果你声明一个属性在一个类的延展中,像这样:
@interface XYZPerson () |
@property NSObject *extraProperty; |
@end |
编译器会在基础类的实现内部自动合成相关的访问器方法和实例变量。
如果你在类延展中添加任何的方法,这些方法必须实现在这个类首次实现的地方。
你也可能使用类延展来添加自定义的实例变量。这是声明在类延展接口括号内的自定义实例变量:
@interface XYZPerson () { |
id _someCustomInstanceVariable; |
} |
... |
@end |
Use Class Extensions to Hide Private Information
一个类的接口主要用来定义一些其他对象希望互相交互的方法。换句话说,对于类来说,就是公共接口。
类延展经常用来扩展公共接口中一些私有的方法或者属性,这些方法或者属性被用在类实现的内部。例如,定义一个只读的属性在接口中是很普遍的情况,但是在实现之上是作为一个readwrite属性在类延展中,以便类内部的方法可以直接更改属性值。
例如,XYZPerson类可能添加一个叫做uniqueIdentifier的属性,这个属性被设计成可以追踪美国的社会保险号码。
在真实的世界里,通过需要大量的工作为一个分配给个人的标识符信息,因此XYZPerson类接口可能声明这个属性是只读的,提供了一些方法来分配一个标识符,像这样:
@interface XYZPerson : NSObject |
... |
@property (readonly) NSString *uniqueIdentifier; |
- (void)assignUniqueIdentifier; |
@end |
这就意味着,另一个对象不可以直接设置uniqueIdentifier的值。如果一个人还没有这样的一个值,必须通过调用assignUniqueIdentifier方法分配一个值。
为了让XYZPerosn类可以在内部改变属性值,在类延展中声明一个属性是合理的,类延展定义在类实现文件的顶部:
@interface XYZPerson () |
@property (readwrite) NSString *uniqueIdentifier; |
@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.
这意味着编译器现在也会合成一个设置器,因此任何在XYZPerson实现中的方法都可以设置属性的值,无论使用setter还是点语法。
通过在XYZPerson实现的源文件中声明一个类延展,对于XYZPerson类的信息来说会保持私有的状态。如果另一个类型的对象试图设置属性值,编译器会报错。
Note: 通过添加类延展,重新声明了属性uniqueIdentifier作为一个readwrite属性,一个setUniqueIdentifier:方法会在运行时存在于每个XYZPerson对象中,不管其他的源代码文件是否意识到了类延展。
如果在其他的源代码文件中的代码试图调用一个私有方法或者设置一个readonly属性时,编译器会抱怨你的,但是你可以为了避免编译错误而利用动态运行时特性用其他的方式来调用一些方法,比如使用一个NSObject提供的performSelector:...方法。你应该避免一个类的层级或者设计在必要的时候;相反,基础类的接口应该总是被定义为“public”公共性质的交互。
如果你打算让私有的方法或者属性可以选择其他的类,如框架中相关的类,你可以在单独的头文件中声明类延展,然后导入类延展需要的源文件。一个类拥有两个头文件并不常见,例如,XYZPerson.h和XYZPersonPrivate.h。当你释放框架的时候,你只是释放了公共的XYZPerson.h头文件。
Consider Other Alternatives for Class Customization
类别和类延展(类扩展)使得对一个存在的类直接添加行为变得很容易,但是有时候这不是最好的选择。
面向对象编程的一个主要目标是编写可重用的代码,这就意味着只要有可能,类应该在各种情况下都是可以重复利用的。如果你创建了一个试图类用来在屏幕上显示信息,例如,考虑一下这个类是否可以在多种情况下被重复利用是个不错的想法。
与其硬编码决定布局或者内容,另一个选择是利用继承,把这些决定留给子类覆盖的特别定义的方法。尽管这样使得重用类变得容易,但是你仍然需要每次想使用原始类的时候创建一个新的子类。
另一个替代品是一个类的代理对象。任何限制重用性的决定都可以被设计成另一个对象的代理,这样使得这些决定在运行时才执行。一个普遍的例子就是一个表追的桌面视图类(NSTableView是OS X系统中的,UITableView是iOS系统中的)。为了使一个普通的桌面视图类(一个显示信息使用一个或多个列和行的对象)变得有用,她让这些关于内容决定的决定这运行时由另一个对象执行。代理模式会在下一章节详细介绍, Working with Protocols。
Interact Directly with the Objective-C Runtime
OC通过动态运行系统提供了她的动态行为。
许多决定,比如信息发送时的方法调用,不是在编译时确定的,相反,是在程序运行时决定的。OC不仅仅是一种被编译到机器代码中的语言,相反,她需要一个运行时系统来执行这些代码。
与运行时系统直接交互时可能的,比如通过给一个对象添加关联引用。和类延展不同的是,关联引用不需要影响原始类的声明和实现,这就意味着,你可以和那些你没有源代码的框架类一起使用。
关联引用使一个对象联系着另一个对象,以一种和属性或者实例变量类似的方法。更多的细节,参见Associative References。学习更多有关OC运行时的内容,参见 Objective-C Runtime Programming Guide。
Exercises练习
-
Add a category to the
XYZPerson
class to declare and implement additional behavior, such as displaying a person’s name in different ways. -
Add a category to
NSString
in order to add a method to draw the uppercase version of a string at a given point, calling through to one of the existingNSStringDrawing
category methods to perform the actual drawing. These methods are documented in NSString UIKit Additions Reference for iOS and NSString Application Kit Additions Reference for OS X. -
Add two
readonly
properties to the originalXYZPerson
class implementation to represent a person’s height and weight, along with methods tomeasureWeight
andmeasureHeight
.Use a class extension to redeclare the properties as
readwrite
, and implement the methods to set the properties to suitable values.