定制现有的类
对象应该有明确的任务,比如塑造特定的信息,显示可视内容或者控制信息流,正如你已经看到的那样,一个类接口定义了其他方面需要与对象进行交互的方式来帮助其完成这些任务
有时,您可能会发现某些情况下您希望为现有类添加扩展的行为,例如,您可能会发现您的应用程序通常需要在可视界面中显示一串字符。 而不是每次需要显示一个字符串都要创建一个字符串绘图对象,如果可以让NSString类本身能够在屏幕上绘制自己的字符。
在这种情况下,将实用程序行为添加到原生主类接口并不总是有意义的, 例如,在任何应用程序中使用任何字符串对象时,绘图功能都不太可能需要,而在NSString的情况下,不能修改原始接口或实现,因为它是一个框架类。
此外,对现有的类进行子类化可能没有意义,因为您可能希望您的绘图行为不仅可用于原始的NSString类,还可以用于该类的任何子类, 像NSMutableString
。而且,尽管NSString
在OSX和iOS上都可用,但是每个平台的绘图代码都需要不同,所以您需要在每个平台上使用不同的子类
相反,Objective-C允许您通过类别和类扩展将自己的方法添加到现有类中。
一: catogory为现有类添加方法
如果您需要在现有的类中基础上添加方法,需要添加功能以便在自己的应用程序中执行某些操作,最简单的方法是使用category
声明类别的语法使用@interface
关键字,就像标准的Objective-C
类描述一样,但是并不表示任何子类的继承。相反,它在圆括号中指定category
的名称,如下所示:
@interface ClassName (CategoryName)
@end
即使您没有原始实现源代码(例如标准Cocoa或Cocoa Touch类),也可以为任何类声明类别,您在类别中声明的任何方法都将可用于原始类的所有实例以及原始类的任何子类。在运行时,类别添加的方法与原始类别实现的方法没有区别。
考虑前面章节中的XYZPerson
类,它具有一个人的名字和姓氏的属性,如果您正在编写记录保存应用程序,则可能会发现您经常需要按姓氏显示人员列表,如下所示
Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
而不是编写代码来生成一个合适的姓氏,firstName字符串,每次你想要显示它,你可以添加一个类别到XYZPerson
类,就像这样
#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
一旦声明了一个类别并实现了方法,就可以使用类的任何实例中的这些方法,就像它们是原始类接口的一部分一样:
#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
除了将方法添加到现有类之外,还可以使用类别将复杂类的实现分割为多个源代码文件,例如,如果几何计算,颜色和渐变等特别复杂,则将自定义用户界面元素的绘制代码放在单独的文件中。或者,您可以为类别方法提供不同的实现,具体取决于您是否正在为OS X或iOS编写应用程序
类别可以用来声明实例方法或类方法,但通常不适用于声明附加属性,在类别接口中包含属性声明的语法是有效的,但不能在类别中声明额外的实例变量.这意味着编译器不会生成任何实例变量,也不会生成任何属性访问器方法。 您可以在类别实现中编写自己的存取方法,但是除非已经存储在原始类中,否则将无法跟踪该属性的值.
将传统属性(由新实例变量支持)添加到现有类中的唯一方法是使用类扩展,如Class Extensions Extend the Internal Implementation
Note
Cocoa和Cocoa Touch包含了一些主要框架类的各种类别
本章介绍中提到的字符串绘图功能实际上已经由OS X的NSStringDrawing类别提供给NSString,它包括
drawAtPoint:withAttributes:
和drawInRect:withAttributes:
methods. For iOS, the UIStringDrawing category includes methods such asdrawAtPoint:withFont:
anddrawInRect:withFont:
1.1: 避免类别方法名称冲突
由于在类别中声明的方法被添加到现有的类中,所以您需要非常小心方法名称。
如果在类别中声明的方法的名称与原始类中的方法相同,或者在同一个类(甚至超类)上的另一个类中的方法,那么行为在运行时使用哪个方法实现是未定义的.如果您使用自己的类使用类别,则这不太可能成为问题,但在使用类别向标准Cocoa或Cocoa Touch类添加方法时可能会导致问题
例如,与远程Web服务一起工作的应用程序可能需要使用Base64编码对字符串进行编码的简单方法。在NSString
上定义一个类别来添加一个实例方法来返回一个Base64
编码的字符串版本是有意义的,所以你可以添加一个名为base64EncodedString
如果你链接到另外一个也在NSString
上定义自己类别的框架,包括它自己的名为base64EncodedString
的方法,就会出现一个问题。在运行时,只有其中一个方法实现会“win”并被添加到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
s示例,您自己的类别可能如下所示
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
这意味着您可以确定您的方法将在运行时使用。由于您的代码如下所示:
NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];