Skip to content

Latest commit

 

History

History
245 lines (135 loc) · 15.7 KB

5.定制已有的类Customizing-Existing-Classes.md

File metadata and controls

245 lines (135 loc) · 15.7 KB

#四、定制已有的类 每一个对象都有它明确的任务,例如封装数据、在界面上展示它的内容、控制信息流等。之前也讲过,类的声明部分定义了它的对象与外部交互的方法,通过这些方法来完成它的任务。

在某些特定的场合,你希望拓展已经存在的类,为其增加一些针对特定场合的有用功能。举个例子,当你的app需要经常在屏幕上显示一个字符串的时候,比起每次都重复创建一个用来显示字符串的对象,更方便的做法是让NSString类本身就拥有将其包含的字符串打印在屏幕上的能力。

在这种情况下,直接修改原来的类接口,为其增加实际功能的做法并不合适。因为大多数情况下我们在程序中使用字符串类型,比如NSString类,并非是为了让其在屏幕上显示,而且NSString类是框架(framework)中的类,你也不能随意去更改它的声明部分和实现部分。

此外如果你想用类的继承特性来为NSString类的子类添加显示的功能,还是会遇到问题。因为如果你这样做,NSString类的其他子类,例如NSMutableString类,则无法使用这个新功能。而且,尽管NSString类可以同时在OS X(macOS)和iOS系统中使用,但是要使它显示在屏幕上的代码却是不同的,这就需要多个子类来适配不同的平台。

为了解决以上问题,Objective-C提供了 类别(categories)class extensions 这两种方式来为已存在的类加入新的方法。

##类的类别 为了让一个已存在的类在你的程序中更加便于使用,你想为一个其添加一个实用的功能,使用类别(categories)会是一个很好的选择。

和普通的Objective-C类的声明一样,声明一个类别的语法要用到 @interface关键字,但不需要声明这个类别的继承关系,取而代之的是,我们需要在圆括号里具体说明这个类别的名字。如下:

@interface ClassName (CategoryName)

@end

任何一个类都可以声明一个类别,即使你不知道它实际代码是如何实现的(比如标准Cocoa或CocoaTouch类)。你在类别中声明的任何方法都可以在它原来类的实例对象和原来类的子类的实例对象中使用。在运行过程中,类别中声明和实现的方法和原来的类中声明的方法没有任何区别。

以前面的章节中提到的XYZPerson类作为例子,这个类有两个属性,分别是firstName和lastName。如果你打算做一个纪录成绩的app的话,你经常需要展示一个把姓放在名前面的清单:

Appleseed, John

Doe, Jane

Smith, Bob

Warwick, Kate

除了每次使用时产生一个last name,first name格式的字符串,你还可以为XYZPerson类添加一个类别,就像这样:

#import "XYZPerson.h"

@interface XYZPerson (XYZPersonNameDisplayAdditions)

- (NSString *)lastNameFirstNameString;

@end

在这个例子中,XYZPersonNameDisplayAdditions这个类别声明了一个新的方法,这个方法返回一个last name,first name格式的字符串。

一个类别通常和它的原类在不同的头文件中定义,并在不同的实现文件中实现。在上面的例子中,这个类别被定义在一个叫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

除了可以为已有类添加新的方法外,你还可以用类别,把那些实现起来很复杂的类分解成多个源代码文件。比方说,你可以把关于在界面上显示的代码,例如几何计算、颜色和倾斜度等等放在另一个文件中,和其他该类的实现代码分开存放。或者,你也可以根据你软件所在的平台(macOS或iOS),来对类别方法提供不同的实现。

实例方法和类方法都可以在类别中声明,但是一般不在类别中声明一个新的属性。尽管你能在类别中声明一个额外属性,但你却不能声明一个相应的实例变量。这意味着编译器既不会自动生成类别中新增加的属性对应的实例变量,也不会自动生成这些属性的存取方法。你可以在分类中实现自己的存取方法,但你写的存取方法只能读取原来类保存的属性。

为一个已有类添加新的属性的唯一方法就是使用类的延伸(class extension),这将在接下来的章节介绍。

注意: Cocoa 和 Cocoa Touch类库中包括了很多原始类的类别。NSString类在OS X系统的屏幕上显示字符串的功能在NSStringDrawing这个类别中实现了。这个类别包含了drawAtPoint:withAttributes:drawInRect:withAttributes:等方法。在iOS系统中,实现这个功能的类别是UIStringDrawing,其中包含了drawAtPoint:withFont:drawInRect:withFont:等方法。

###避免类别中方法名的冲突 因为类别中的方法名是添加在原来的类中的,所以你需要很谨慎的为这些方法命名,避免它们与原来的类的方法发生冲突。

如果一个定义在类别中的方法名和原来的类中的一个方法名重复了,或者和该类的另一个类别的一个方法名重复了,在运行过程中具体调用哪个方法来实现会是随机决定的。这种情况很少在为自己的类添加类别时发生,而经常在为Cocoa 和 Cocoa Touch类库的类添加方法时发生。

举个例子,假如一个app需要从一个远程网络服务器中获取数据,这就需要一个方法来为字符串编码成Base64形式。这时候就需要为NSString类添加一个类别,并且在其中添加一个返回编码后的字符串的base64EncodedString方法。但当你把其他类库加进来时,问题就发生了。其他类库也许也会为NSString这个类添加一个叫base64EncodedString的方法。那么在运行的时候,只有一个方法会被使用,而这个方法到底是哪一个,是不确定的。

还有另一个问题,就是当一个类添加了一个方法之后,有可能在接下来的代码迭代版本中,又添加了另一个同名的方法,这样新旧两个方法就冲突了。举个例子,NSSortDescriptor这个类中有一个叫initWithKey:ascending:的初始化方法,这个方法在早期的OS X和iOS版本中是没有相关的类方法。按照命名传统,这个类方法应该叫做sortDescriptorWithKey:ascending:,所以你可以在NSSortDescriptor这个类上添加这个类方法。在早期的OS X和iOS版本中这样做是没有问题的,但是在Mac OS X 10.6和iOS4.0版本之后,官方类库中就为NSSortDescriptor这个添加了sortDescriptorWithKey:ascending:这个类方法,这就意味着在新版本的系统中你自己命名的方法和类库中的方法冲突了,必须要手动解决这个问题。

为了避免这些不必要的冲突,很有必要为新添加的方法名添加一个前缀,特别是那些在官方类库中的类。这种做法有点像为你自己的类名添加一个前缀。你可以使用你名字的首字母来作为你的前缀,包括方法名和类名的前缀。除此之外,还可以用下划线把前缀和方法名连接起来。在NSSortDescriptor这个例子中,你自定义的类别可以长这样:

@interface NSSortDescriptor (XYZAdditions)

+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;

@end

这样做可以确保在运行的时候会调用正确的方法而没有产生歧义。你的方法是这样被调用的:

 NSSortDescriptor *descriptor =

               [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];

##类的拓展 类的拓展和类别有点相似,不同的是,类的拓展只能在你有这个类的实现的源代码时使用(类的拓展和类本身是在同一时间编译的)。因为类的拓展中声明的方法需要在该类的@implementation代码块中实现,也就是说,你不能为Cocoa 和 Cocoa Touch类库中的类添加拓展,比如NSString类。

为一个类添加拓展的语法和添加类别有点相似,长这样:

@interface ClassName ()

 

@end

因为括号中并没有添加其他名字,所以类的拓展经常被当作类的一个匿名的类别。

不像普通的类别,类的拓展可以添加它的属性和实例变量。用拓展定义一个属性的语法是这样的:

@interface XYZPerson ()

@property NSObject *extraProperty;

@end

编译器会自动实现拓展中出现的属性和实例变量的get方法和set方法。如果你在拓展中定义了一个新的方法,你必须在该类的原实现代码中实现这个方法。拓展中也可以增加新的实例变量,新增添的实例变量须在拓展中用花括号括起来:

@interface XYZPerson () {

    id _someCustomInstanceVariable;

}

...

@end

###使用类的拓展隐藏私有信息 原有的类的声明部分定义了它和外界交互的方式,这些信息都是希望外部可以访问的。也就是说,这是类的公有部分。

类的拓展一般用来添加不希望出现在公有部分的 私有方法私有属性 ,这些私有成员只在类的内部使用。举个例子,在公有部分定义一个readonly的公有属性,再在类的拓展中定义一个readwrite的私有属性,这样,外部就可以访问到一个类内部经常变化的一个属性,却不会去改变它。

XYZPerson类作为例子,为了标记能标记一个人的身份证号,我们为它添加一个叫uniqueIdentifier的属性,要更改一个人的身份证号是很难的,所以我们把这个属性设置为只读的,再声明一个为一个人分配身份证号的方法,代码这样写:

@interface XYZPerson : NSObject

...

@property (readonly) NSString *uniqueIdentifier;

- (void)assignUniqueIdentifier;

@end

这意味着uniqueIdentifier这个属性不能被外部直接改变。如果一个人还没有一个身份证号,这时候就要调用assignUniqueIdentifier为他分配一个身份证号。如果想要在内部改变这个人的身份证号,可以在类的拓展中重新声明一个可读写的属性,这需要写在这个类的.m文件的最上面。

@interface XYZPerson ()

@property (readwrite) NSString *uniqueIdentifier;

@end

 

@implementation XYZPerson

...

@end

注意:readwrite可读写性是可以省略的,因为声明一个属性时默认就是可读写的,这里加上是为了和公有部分的只读的属性区分清楚。

这意味着编译器会实现这个属性的set方法,所以在这个函数的实现代码中既可以用 .语法 来修改这个属性的值,也可以使用set方法来改变它。通过在XYZPerson类的.m文件中声明它的拓展,我们可以定义这个类的一些私有信息。如果其他的对象想要对这些私有变量使用set方法改变它的值,编译器就会报错。

注意:(关于set方法是什么)通过上面的代码我们在XYZPerson类中重新声明了一个可读写的uniqueIdentifier属性,这时编译器就会自动实现一个叫setUniqueIdentifier: 的set方法,无论这个类的实现部分知不知道(be aware of)类的拓展,这个方法在XYZPerson的每一个对象中都能被调用。 当对象想要调用它的私有方法或者想要对读的属性使用set方法时,编译器会发出警告(complain),但是还是有方法可以调用一个私有方法,比如用NSObject提供的performSelector:...方法。在设计类时,我们要设计好那哪些方法是公有的,哪些是私有的,以免出现不必要的麻烦。 如果你想让私有方法和变量在不同的类中共用的话,你可以把类的拓展写在一个单独的.h文件中,再在需要的时候引用这个头文件。拥有两个头文件的类并不常见。如果一个类同时拥有如XYZPerson.hXYZPersonPrivate.h两个头文件,你应该只把XYZPerson.h头文件暴露出来。

##定制类的其他方法 类别和拓展提供了为已有类添加方法的简单途径,但是有时候这两个不是最佳的选择。

面向对象编程其中一个目标就是要重复利用代码,这意味着类需要在大多数情况下都可重复利用。举个例子,如果你要设计一个把对象显示在屏幕上的类,那这个类最好最好要在多数的情况下都能使用。对于对象的布局和内容等这些很难写的代码,一种方法是使用类的继承特性,这需要在子类中重载一些方法。尽管这样相对地使得重用代码更简单,但是你仍然要在不同的情况设计不同的子类。

另一种方法是使用 代理(delegate) 。任何可能重复使用的操作都可以代理给另一个对象,这个对象将在运行的时候决定是否执行那些操作。一个常用的例子就是表视图(OS X系统下的NSTableView类和iOS系统的UITableView类)。为了使一个普通的表视图能在很多地方发挥作用,它把一些方法代理了给另一个使用它的对象,在下一章我们将详细讨论代理的使用,请见 Working with Protocols。

###Objective-C的运行时 Objective-C提供一种非常灵活的模式——运行时系统。

就像一些方法在得到信息时才会被调用,很多的操作都不是在编译的时候决定的,而是在app运行的时候。Objective-C不仅仅是一门把代码编译成机器二进制码的高级语言,它还有一套运行时系统来执行那些代码。

我们可以直接操作运行时系统,例如为对象添加 associative references。不像类的拓展,associative references并不影响类的声明和实现,这意味着可以对类库中的类和你没有它实现代码的类使用它。

associative references把一个对象和另一个对象连起来,与属性和实例变量是用的是相似的方式。想要了解更多的话请参考associative reference。如果想了解更多关于运行时系统的,请看Objective-C Runtime Programming Guide。

##练习 1.为XYZPerson类添加一个类别,并且实现一个新的方法,例如以不同的方式显示一个人的名字。 2.为NSString类添加一个类别,添加一个把字符串全部转换成大写字母的方法,并且调用已存在的方法NSStringDrawing把它在屏幕上显示出来。 3.为XYZPerson类添加两个只读的属性,用来表示一个人的身高的体重,以及两个叫measureWeightmeasureHeight的方法。用类的拓展重新声明这两个属性为可读写型,并且实现上面两个方法。