第11章 分类和协议
在本章中,你将学习如何通过使用分类(category)以模块的方式向类添加方法,以及如何创建标准化的方法列表供其他人实现。
11.1 分类
有时候在处理类定义时,可能想要为其添加一些新方法。例如,对于Fraction类,除了将两个分数相加的add:方法之外,还想要拥有将两个分数相减、相乘、相除的方法。
再举一个例子,假如你参与一个大型程序设计项目,并且作为该项目的一部分,正在定义一个新类,它包含许多方法。你的任务就是为该类编写处理文件系统的方法。其他项目成员的任务负责以下方法:创建和初始化该类实例、对该类中的对象执行操作以及在屏幕上绘制该类对象的表示。
最后一个例子,假如你已经知道如何使用库中的类(例如,Foundation的数组类,名为NSArray),并且认识到你希望该类实现了一个或多个方法。当然,可以编写NSArray类的新子类并实现新方法,但是可能存在更简单的方式。
以上所有情况的实用解决方案是一个:分类。分类提供了一种简单的方式,用它可以将类的定义模块化到相关方法的组或分类中。它还提供了扩展现有类定义的简便方式,并且不必访问类的源代码,也无需创建子类。分类是一个功能强大且简单的概念。
回到第一个例子,展示如何为Fraction类添加新分类,以处理基本的四则数学运算。首先,展示原始的Fraction接口部分:
import<Foundation/Foundation.h>
//Define the Fraction class
@interface Fraction:NSObject
{
int numerator;
int denominator;
}
@property int numerator, denominator;
-(void)setTo:(int)n over:(int)d;
-(Fraction)add:(Fraction)f;
-(void)reduce;
-(double)convertToNum;
-(void)print;
@end
然后,从接口部分删除add:方法,并将其添加到新分类,同时添加其他三种要实现的数学运算。新MathOps分类的接口部分应该如下所示:
import“Fraction.h”
@interface Fraction(MathOps)
-(Fraction)add:(Fraction)f;
-(Fraction)mul:(Fraction)f;
-(Fraction)sub:(Fraction)f;
-(Fraction)div:(Fraction)f;
@end
注意,这既是接口部分的定义,也是现有接口部分的扩展。因此,必须包括原始接口部分,这样编译器就知道Fraction类(除非直接将新分类结合到原始Fraction.h头文件,这是一种选择)。
在#import之后,有下面这一行:
@interface Fraction(MathOps)
这告诉编译器你正在为Fraction类定义新的分类,而且它的名称为MathOps。这个名称括在类名称之后的一对圆括号中。注意此处没有列出Fraction的父类,因为编译器已从Fraction.h中知道此内容。而且,你没有向编译器告知实例变量,因为在以前定义的接口部分中已经这样做了。实际上,如果尝试列出父类或实例变量,将收到编译器发出的语法错误。
这个接口部分告知编译器,你正在名为MathOps的分类下为名为Fraction的类添加扩展。MathOps分类包括4个实例方法:add:、mul:、sub:和div:。每种方法均使用一个分数作为参数并返回一个分数。
可以将所有方法的定义放在一个实现部分。也就是,可以在一个实现文件中定义Fraction.h接口部分中的所有方法,以及MathOps分类中的所有方法。或者,在单独的实现部分定义分类的方法。在这种情况下,这些方法的实现部分还必须找出方法所属的分类。与接口部分一样,通过将分类名称括在类名称之后的圆括号中来确定方法所属的分类,如下所示:
@implementation Fraction(MathOps)
//code for category methods
……
@end
在代码清单11-1中,新的MathOps分类的接口和实现部分组合在一起,连同测试例程都放在一个文件中。
代码清单11-1 MathOps分类和测试程序
import“Fraction.h”
@interface Fraction(MathOps)
-(Fraction)add:(Fraction)f;
-(Fraction)mul:(Fraction)f;
-(Fraction)sub:(Fraction)f;
-(Fraction)div:(Fraction)f;
@end
@implementation Fraction(MathOps)
-(Fraction)add:(Fraction)f
{
//To add two fractions:
//a/b+c/d=((ad)+(bc))/(b*d)
Fraction*result=[[Fraction alloc]init];
int resultNum, resultDenom;
resultNum=(numerator*f.denominator)+
(denominator*f.numerator);
resultDenom=denominator*f.denominator;
[result setTo:resultNum over:resultDenom];
[result reduce];
return result;
}
-(Fraction)sub:(Fraction)f
{
//To sub two fractions:
//a/b-c/d=((ad)-(bc))/(b*d)
Fraction*result=[[Fraction alloc]init];
int resultNum, resultDenom;
resultNum=(numerator*f.denominator)-
(denominator*f.numerator);
resultDenom=denominator*f.denominator;
[result setTo:resultNum over:resultDenom];
[result reduce];
return result;
}
-(Fraction)mul:(Fraction)f
{
Fraction*result=[[Fraction alloc]init];
[result setTo:numerator*f.numerator
over:denominator*f.denominator];
[result reduce];
return result;
}
-(Fraction)div:(Fraction)f
{
Fraction*result=[[Fraction alloc]init];
[result setTo:numerator*f.denominator
over:denominator*f.numerator];
[result reduce];
return result;
}
@end
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
Fraction*a=[[Fraction alloc]init];
Fraction*b=[[Fraction alloc]init];
Fraction*result;
[a setTo:1 over:3];
[b setTo:2 over:5];
[a print];NSLog(@);[b print];NSLog(@“——”);
result=[a add:b];
[result print];
NSLog(@“n”);
[result release];
[a print];NSLog(@);[b print];NSLog(@“——”);
result=[a sub:b];
[result print];
NSLog(@“n”);
[result release];
[a print];NSLog(@);[b print];NSLog(@“——”);
result=[a mul:b];
[result print];
NSLog(@“n”);
[result release];
[a print];NSLog(@);[b print];NSLog(@“——”);
result=[a div:b];
[result print];
NSLog(@“n”);
[result release];
[a release];
[b release];
[pool drain];
return 0;
}
代码清单11-1输出
1/3
+
2/5
11/15
1/3
-
2/5
-1/15
1/3
*
2/5
2/15
1/3
/
2/5
5/6
再次注意,以下语句在Objective-C语言中是合法的:
[[a div:b]print];
这行代码将直接打印分数a除以b的结果,因此避免了对变量result的中间赋值,在代码清单11-1中有这项操作,但需要执行这个中间赋值,这样可以获得结果Fraction,并随后释放它的内存。否则,每次对分数执行数学运算,程序都会泄漏一些内存。
代码清单11-1把新分类的接口和实现部分与测试程序放在一个文件中。前面提到过,这个分类的接口部分可以放在原始的Fraction.h头文件中(这样,所有方法都在一个位置声明),也可以放在自己的头文件中。
如果将分类放到一个主类定义文件中,那么这个类的所有用户都将访问这个分类中的方法。如果不能直接修改原始的头文件(考虑从一个库向现有类添加一个分类,如第二部分“Foundation框架”中所示),除了单独保存它之外,别无选择。
关于分类的一些注意事项
关于分类有几点值得注意。首先,尽管分类可以访问原始类的实例变量,但是它不能添加自身的任何变量。如果需要添加变量,可以考虑创建子类。
另外,分类可以重载该类中的另一个方法,但是通常认为这种做法是拙劣的设计习惯。其一,重载了一个方法之后,再也不能访问原来的方法。因此,必须小心地将被重载方法中的所有功能复制到替换方法中。如果确实需要重载方法,正确的选择可能是创建子类。如果在子类中重载方法,仍然可以通过向super发送消息来引用父类的方法。因此,不必了解要重载方法的复杂内容,就能够调用父类的方法,并向子类的方法添加自己的功能。
如果喜欢,可以拥有许多分类,只要遵守此处指出的规则。如果一个方法定义在多个分类中,该语句不会指定使用哪个分类。
和一般接口部分不同的是,不必实现分类中的所有方法。这对于程序扩展很有用,因为可以在该分类中声明所有方法,然后在一段时间之后才实现它。
记住,通过使用分类添加新方法来扩展类不仅会影响这个类,同时也会影响它的所有子类。例如,如果为根对象NSObject添加新方法,就存在潜在的危险性,因为每个人都将继承这些新方法,无论你是否愿意。
通过分类为现有类添加新方法可能对你有用,但它们可能和该类的原始设计或意图不一致。例如,通过添加一个新分类和一些方法修改Square类的定义,将Square变成Circle(确实有些夸张),并非好的编程习惯。
同样,对象/分类命名对必须是唯一的。但是,在给定的Objective-C名称空间中,只能存在一个NSString(私有的)分类。这样做可能比较复杂,因为,Objective-C名称空间是程序代码和所有库框架和插件共享的。对于编写屏幕保护首选窗格和其他插件的Objective-C程序员,这尤为重要,因为这些代码将插入到他们无法控制的应用程序或框架代码中。