自定义现有类(Customizing Existing Classes)

对象应该有明确定义的任务,例如构建具体信息、显示视觉内容或者控制信息流动。一个类接口定义其他与对象相互作用的方式,帮助对象完成这些任务。
有时候,你会发现通过添加行为来扩展现有类只有在某些情况下有用。例如,你可能发现你的应用程序经常需要在视觉界面显示一个字符串。而不是每次需要显示一个字符串时,就创建一些字符串绘图对象。如果可以赋予NSString类自己在屏幕上绘制自己的特性,这将更加有意义。
在这种情况下,添加公用行为到原始主要类接口并不总是有意义。在应用程序中大多数使用的string对象不需要绘图功能,例如,在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
你可以添加一个类别到XYZPerson 类,而不用每次需要显示时,写代码生成lastName, firstName字符串:
#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中描述的。
注:Cocoa 和Cocoa Touch包含一些主要框架类的各种类别。

本章介绍中提到的字符串绘制功能,在OS X的NSStringDrawing 类别中已经为NSString提供,其中包括drawAtPoint:withAttributes:和drawInRect:withAttributes: 方法。对于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版本和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];


类extension拓展内部实现

类extension和类别有些相似,但是累拓展只能在编译时(类编译的同时类extension也同时编译)有源代码的情况下才能添加到类中。类extension声明的方法在原始类的@implementation block中实现,所以,不能在框架类中声明类extension,例如NSString等Cocoa或Cocoa Touch类。
声明类extension和声明类别的语法类似,如下:
@interface ClassName ()
 
@end
因为括号中没有名字,所以类extensions 通常被称为匿名类。
与常规类别不同,一个类extension 可以添加自定义的属性和实例变量。如果在一个类extension 中声明一个属性,如下:
@interface XYZPerson ()
@property NSObject *extraProperty;
@end
编译器会在主类的实现中自动生成相关的访问方法以及实例变量。
如果在一个类extension添加任何方法,则必须在类的主要实现中实现这些方法。
可以使用类extension添加自定义实例变量。在类extension接口的花括号内声明这些变量。
@interface XYZPerson () {
    id _someCustomInstanceVariable;
}
...
@end


使用类extension隐藏私有信息

类的主要接口是用来定义与其他类进行交互的方式。换句话说,它是类的公共接口。
类extension通常用于扩展公共接口的额外私有方法或类本身实现中使用的属性。例如,通常在接口定义一个只读属性,在实现上定义的类extension中是读写属性,这样类内部方法可以直接改变属性值。
举个例子,XYZPerson 类可以添加一个名为uniqueIdentifier的属性,旨在跟踪类似美国社会保险号信息。
在现实世界中,给一个人分配唯一标识符通常需要大量的文书工作,所以XYZPerson 类接口声明这个属性为只读,并提供一些方法请求分配标识符,如下:
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
这表明其他对象不能直接设置uniqueIdentifier 。如果一个人没有uniqueIdentifier ,则必须调用assignUniqueIdentifier 方法请求分配一个标识符。
为了使XYZPerson 类能够改变内部属性,有必要在类实现文件的上方,类extension 中重新定义属性。
@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
 
@implementation XYZPerson
...
@end
注:读写属性是可选的,因为它是默认的。为了清晰显示,可以在重新定义属性时使用它。
这表明编译器还将合成一个setter方法,所以在XYZPerson 实现中的任何方法可以通过setter或点语法直接设置属性。
通过在XYZPerson 实现的源代码文件中声明类extension ,对于XYZPerson 类来说信息一直是私有的。如果其他类型的对象试图设置属性,编译器将生成错误。
注:通过添加上面所示的类extension ,重新定义uniqueIdentifier 属性为读写属性,在运行时每个XYZPerson 对象中都有一个setUniqueIdentifier: 方法,不论其他源代码是否知道类extension 。
如果其他源代码文件中的任何一个试图调用私有方法或设置只读属性,编译器将抱怨?但可以利用动态运行时功能以其他方式调用这些方法来避免编译器错误,例如,使用NSObject提供的performSelector:... 方法。应该避免一个类层次结构或设计,相反,主类接口必须定义正确的“公共”接口。

如果希望“私有”方法或属性对其他类可用,例如在一个框架中相关的类,可以在单独的头文件中声明类extension 并在需要时导入。一个类有两个头文件非常常见,例如XYZPerson.h 和XYZPersonPrivate.h。当释放框架时,只释放 XYZPerson.h这个公共头文件。


考虑自定义类的其他替代方法

类别和类extensions 使往现有类中直接添加行为变得更加容易,但有时候这并不是最好的选择。
面向对象编程的一个主要目标是编写可重用的代码,这意味着在不同情况下只有有可能,类必须可重用。例如,你创建一个视图类来描述屏幕上显示信息的对象,这个类在多种情况下是否可用。
一个替代方法是利用继承,让子类覆盖这些专门设计的方法,而不是利用硬编码来决定布局和内容。虽然这种方式使重用类相对容易,但每次想使用原始类时,必须创建一个新的子类。

类的另一个替代方法是使用委托对象。任何决策可能会限制重用可以委托给另一个对象,这可以留到运行时做决定。一个常见的例子是标准表视图类(在OS X 中是NSTableView ,在iOS中是UITableView )。为了使通用表视图(一个利用行和列来显示信息的对象)有用,让其他对象在运行时决定它的内容。在下一章将详细介绍委托与使用协议。


直接与Objective-C运行系统交互

Objective-C通过Objective-C运行系统提供动态行为。
许多决策,例如当消息发送时哪些方法被调用,不是在编译时而是在应用程序运行时决定。Objective-C不仅仅是编译到机器代码的一种语言。相反,它需要一个运行系统来执行这些代码。
可以与运行系统直接交互,例如通过添加关联引用到一个对象。不像类extension,关联引用不影响原始类的声明和实现,这表明可以在框架类中使用它们,虽然不能访问原始源代码。

一个关联引用链接两个对象,有点类似于属性或实例变量。更多信息,查看关联引用。了解更多关于Objective-C 运行时,可查看Objective-C Runtime Programming Guide。


官方原文:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值