上一章Objective-C编程快速入门教程请查看:封装数据?
自定义现有的类对象应该有明确定义的任务,如建模特定信息、显示可视内容或控制信息流。正如你已经看到的,类接口定义了期望其他人与对象交互以帮助其完成这些任务的方式。
有时,你可能发现希望通过添加仅在某些情况下有用的行为来扩展现有的类。例如,你可能会发现你的应用程序经常需要在可视界面中显示一串字符。与其每次需要显示字符串时创建一些string-drawing对象来使用,不如让NSString类本身能够在屏幕上绘制自己的字符。
在这种情况下,将实用程序行为添加到原始的主类接口并不总是有意义的。例如,在应用程序中大多数时候不太可能需要绘图功能,而在NSString的情况下,你不能修改原始接口或实现,因为它是一个框架类。
此外,子类化现有类可能没有意义,因为你可能希望绘图行为不仅对原始NSString类可用,而且对该类的任何子类都可用,比如NSMutableString。而且,尽管NSString在OS X和iOS上都可用,但每个平台的绘图代码需要不同,因此你需要在每个平台上使用不同的子类。
相反,Objective-C允许你通过类别和类扩展将你自己的方法添加到现有的类中。
类别:向现有类添加方法如果你需要向现有的类添加一个方法,可能需要添加一些功能,以便在自己的应用程序中更容易地执行某些操作,那么最简单的方法是使用类别。
声明类别的语法使用@interface关键字,就像标准的Objective-C类描述一样,但不表示继承自子类。相反,它在括号中指定类别的名称,如下所示:
@interface ClassName (CategoryName)
@end
类别可以为任何类声明,即使你没有原始实现源代码(例如标准Cocoa或Cocoa Touch类)。你在类别中声明的任何方法都将对原始类的所有实例以及原始类的任何子类可用。在运行时,由类别添加的方法与由原始类实现的方法没有区别。
考虑前面几章中的XYZPerson类,它具有人名和姓氏的属性。如果你正在编写一个记录保存应用程序,你可能会发现经常需要按姓氏显示人员列表,如下所示:
Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
每次你想要显示一个合适的lastName, firstName字符串时,你不需要写代码来生成它,你可以向XYZPerson类添加一个类别,就像这样:
#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end
在本例中,XYZPersonNameDisplayAdditions 类别声明了一个额外的方法来返回必要的字符串。
类别通常在单独的头文件中声明,并在单独的源代码文件中实现。对于XYZPerson,你可以在一个名为XYZPerson+XYZPersonNameDisplayAdditions.h的头文件中声明类别。
即使类别添加的任何方法对类及其子类的所有实例都可用,你仍需要在希望使用附加方法的任何源代码文件中导入类别头文件,否则将遇到编译器警告和错误。
category实现可能是这样的:
#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
除了向现有类添加方法之外,还可以使用类别将复杂类的实现分割到多个源代码文件中。例如,如果几何计算、颜色和梯度等特别复杂,你可以将一个自定义用户界面元素的绘图代码放在一个单独的文件中,以供实现的其他部分使用。或者,你可以为category方法提供不同的实现,这取决于你是在为OS X还是iOS编写应用程序。
类别可用于声明实例方法或类方法,但通常不适用于声明其他属性。在类别接口中包含属性声明是有效的语法,但是不能在类别中声明额外的实例变量。这意味着编译器不会合成任何实例变量,也不会合成任何属性访问器方法。你可以在category实现中编写自己的访问器方法,但是你无法跟踪该属性的值,除非它已经由原始类存储。
向现有类添加由新实例变量支持的传统属性的惟一方法是使用类扩展,如类扩展扩展内部实现中所述。
注意:Cocoa和Cocoa Touch包含了一些主要框架类的各种类别。
本章引言中提到的字符串绘制功能实际上已经由OS X的NSStringDrawing类别为NSString提供了,其中包括drawAtPoint:withAttributes:和drawInRect:withAttributes: methods。对于iOS, UIStringDrawing类包括drawAtPoint:withFont:和drawInRect:withFont:等方法。
避免类别方法名称冲突
因为类别中声明的方法是添加到现有类中的,所以你需要非常小心地选择方法名。
如果在类别中声明的方法的名称与原始类中的方法相同,或者与同一类(甚至超类)上的另一个类别中的方法相同,则未定义在运行时使用哪个方法实现。如果你在自己的类中使用类别,那么这不大可能成为问题,但是在使用类别向标准Cocoa或Cocoa Touch类添加方法时可能会造成问题。
例如,使用远程web服务的应用程序可能需要一种使用Base64编码编码字符串的简单方法。在NSString上定义一个类别以添加一个实例方法来返回一个base64编码的字符串版本是有意义的,因此你可以添加一个名为base64EncodedString的方便方法。
如果你链接到另一个框架,该框架恰好也在NSString上定义了它自己的类别,包括它自己的方法base64EncodedString,就会出现问题。在运行时,只有一个方法实现会“赢”并被添加到NSString中,但哪个是未定义的。
如果向Cocoa或Cocoa Touch类中添加方便的方法,然后在以后的版本中添加到原始类中,可能会出现另一个问题。例如,描述对象集合如何排序的NSSortDescriptor类,总是有一个initWithKey:ascending:初始化方法,但是在早期的OS X和iOS版本中没有提供相应的类工厂方法。
按照惯例,类工厂方法应该被称为sortDescriptorWithKey:ascending:,因此你可能选择在NSSortDescriptor上添加一个类别来提供这个方法,以提供方便。这是你期望在旧版本的OS X和iOS,但与Mac OS X版本10.6的发布和iOS 4.0, sortDescriptorWithKey:ascending:方法添加到原始NSSortDescriptor类,这意味着你现在得到一个命名冲突当应用程序运行在这些或更高的平台。
为了避免未定义的行为,最佳实践是在框架类的类别中为方法名添加前缀,就像你应该为自己的类名添加前缀一样。你可以选择使用与类前缀相同的三个字母,但方法名通常使用小写字母,然后是下划线,然后是方法名的其余部分。对于NSSortDescriptor的例子,你自己的类别可能是这样的:
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
这意味着你可以确保你的方法将在运行时被使用。消除了歧义,因为你的代码现在看起来像这样:
NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];
类扩展:扩展类的内部实现类扩展与类别具有某些相似性,但是只能将其添加到你在编译时拥有源代码的类中(该类与该类扩展同时进行编译)。 由类扩展声明的方法是在原始类的@implementation块中实现的,因此,例如,你不能在框架类(例如Cocoa或NSString之类的Cocoa或Cocoa Touch类)上声明类扩展。
声明类扩展的语法类似于类别的语法,如下所示:
@interface ClassName ()
@end
因为括号中没有给出名称,所以类扩展通常被称为匿名类别。
与常规类别不同,类扩展可以将自己的属性和实例变量添加到类中。如果你在类扩展中声明一个属性,像这样:
@interface XYZPerson ()
@property NSObject *extraProperty;
@end
编译器将在主类实现中自动合成相关的访问器方法和实例变量。
如果在类扩展中添加任何方法,这些方法必须在类的主实现中实现。
也可以使用类扩展来添加自定义实例变量。这些在类扩展接口的大括号中声明:
@interface XYZPerson () {
id _someCustomInstanceVariable;
}
...
@end
使用类扩展来隐藏私有信息
类的主接口用于定义其他类与它交互的方式。换句话说,它是类的公共接口。
类扩展通常用于使用附加的私有方法或属性来扩展公共接口,以便在类本身的实现中使用。例如,通常在接口中将属性定义为readonly,但在实现上面声明的类扩展中定义为readwrite,以便类的内部方法可以直接更改属性值。
例如,XYZPerson类可能会添加一个名为uniqueIdentifier的属性,用于跟踪信息。
通常需要大量的书面工作来为现实世界中的个人分配唯一的标识符,所以XYZPerson类接口可能会将此属性声明为readonly,并提供一些方法来请求分配标识符,如下所示:
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
这意味着不可能由另一个对象直接设置uniqueIdentifier。如果一个人还没有一个标识符,那么必须通过调用assignUniqueIdentifier方法来请求分配一个标识符。
为了让XYZPerson类能够在内部更改属性,有必要在类的实现文件顶部定义的类扩展中重新声明属性:
@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end
注意:readwrite属性是可选的,因为它是默认的。为了清晰起见,你可能希望在重新声明属性时使用它。
这意味着编译器现在也将合成一个setter方法,因此XYZPerson实现中的任何方法都能够直接使用setter或dot语法设置属性。
通过在XYZPerson实现的源代码文件中声明类扩展,信息对XYZPerson类保持私有。如果另一种类型的对象试图设置属性,编译器将生成一个错误。
注意:通过添加上面所示的类扩展,将uniqueIdentifier属性重新声明为readwrite属性,setUniqueIdentifier:方法将在运行时存在于每个XYZPerson对象上,而不管其他源代码文件是否知道类扩展。
如果其他源代码文件之一中的代码尝试调用私有方法或设置只读属性,则编译器会抱怨,但是可以避免编译器错误并利用动态运行时功能以其他方式(例如通过使用 由NSObject提供的performSelector:…方法之一。 在必要时,应避免使用类层次结构或设计; 相反,主要类接口应始终定义正确的“公共”交互。
如果你打算使用“私有”方法或属性来选择其他类,例如框架中的相关类,你可以在单独的头文件中声明类扩展,并将其导入需要它的源文件中。一个类通常有两个头文件,例如XYZPerson.h和XYZPersonPrivate .h。当你发布框架时,你只发布公共的XYZPerson.h头文件。
考虑类定制的其他替代方法类别和类扩展使直接向现有类添加行为变得很容易,但有时这不是最佳选择。
面向对象编程的主要目标之一是编写可重用的代码,这意味着类应该在各种可能的情况下可重用。例如,如果你正在创建一个视图类来描述一个在屏幕上显示信息的对象,那么最好考虑这个类是否可以在多种情况下使用。
与对布局或内容进行硬编码决策不同,一种替代方法是利用继承并将这些决策留在专门设计用于被子类覆盖的方法中。尽管这使得重用该类变得相对容易,但是每次你想要使用原始类时,仍然需要创建一个新的子类。
另一种方法是类使用委托对象。任何可能限制可重用性的决策都可以委托给另一个对象,让它在运行时做出这些决策。一个常见的例子是一个标准的表视图类(用于OS X的NSTableView和用于iOS的UITableView)。为了使泛型表视图(使用一个或多个列和行显示信息的对象)有用,它将关于其内容的决策留到运行时由另一个对象决定。下一章“使用协议”将详细讨论委托。
直接与Objective-C运行时交互
Objective-C通过Objective-C运行时系统提供它的动态行为。
许多决策(比如发送消息时调用哪些方法)不是在编译时做出的,而是在应用程序运行时确定的。Objective-C不仅仅是一种编译成机器码的语言。相反,它需要一个适当的运行时系统来执行这些代码。
可以直接与这个运行时系统交互,例如向对象添加关联引用。与类扩展不同,关联的引用不会影响原始的类声明和实现,这意味着你可以将它们与无法访问原始源代码的框架类一起使用。
关联引用以类似于属性或实例变量的方式将一个对象与另一个对象链接起来。有关更多信息,请参见关联引用。要了解有关Objective-C运行时的更多信息,请参阅Objective-C运行时编程指南。
练习1、向XYZPerson类添加一个类别来声明和实现其他行为,比如以不同的方式显示人名。
2、向NSString添加一个类别,以便添加一个方法来在给定点绘制一个字符串的大写版本,通过调用一个现有的NSStringDrawing类别方法来执行实际的绘制。这些方法在iOS的NSString UIKit附加参考和OS X的NSString应用程序工具包附加参考中有记录。
3、在原来的XYZPerson类实现中添加两个readonly属性来表示一个人的身高和体重,以及测量体重和身高的方法。
【自定义现有的类(类别和扩展 – Objective-C编程快速入门教程)】使用类扩展将属性重新声明为readwrite,并实现将属性设置为适当值的方法。
推荐阅读
- 协议的定义和使用 – Objective-C编程快速入门教程
- 封装数据 – Objective-C编程快速入门教程
- 使用对象 – Objective-C编程快速入门教程
- 定义类 – Objective-C编程快速入门教程
- 入门编程介绍 – Objective-C编程快速入门教程
- Core Foundation编程概念全解
- Objective-C线程技术(线程同步和线程安全)
- 8086程序查找8位数字的总和
- 8086程序,用于确定两个数组的对应元素的总和