第8章 继承
在本章将学到使面向对象编程如此强大的一个重要原理。通过继承这个概念,将学会如何使用现有的类定义并使其适于自己的应用程序。
8.1 一切从根类开始
在第3章“类、对象和方法”中学过父类的概念。父类自身也可以有父类。没有父类的类位于类层次结构的最顶层,称为根(root)类。在Objective-C中,允许定义自己的根类,但通常你不想这么做,而是想要利用现有的类。至此我们所定义的类都属于名为NSObject的根类的派生类,这个根类通常如下在接口文件中指定:
@interface Fraction:NSObject
……
@end
类Fraction就是从类NSObject派生来的。因为NSObject是层次结构的最顶端(也就是,它上面没有任何类),因此称它为根类,如图8-1所示。类Fraction称为子或者子类(subclass)。
图 8-1 根类和子类
从术语的角度而言,可以将一个类称做子类和父类。相似地,还可以将类称作子类和超类。你应该熟悉这两种术语。
只要定义一个新类(不是一个新的根类),该类都会继承一些属性。例如,很明显父类的所有实例变量和方法都成为新类定义的一部分。这意味着子类可以直接访问这些方法和实例变量,就像直接在类定义中定义了这些子类一样。
举一个简单的例子,虽然不太自然,但有助于解释继承这个关键概念。
下面是一个名为ClassA的对象的声明,它有一个方法initVar:
@interface ClassA:NSObject
{
int x;
}
-(void)initVar;
@end
initVar方法简单地把100赋值给ClassA的实例变量:
@implementation ClassA
-(void)initVar
{
x=100;
}
@end
现在,再定义一个名为ClassB的类:
@interface ClassB:ClassA
-(void)printVar;
@end
声明的第一行
@interface ClassB:ClassA
说明ClassB并非NSObject的子类,而是ClassA的子类。所以,尽管ClassA的父类(或超类)是NSObject,但是ClassB的父类却是ClassA,如图8-2所示。
图 8-2 子类及超类
如图8-2所示,根类没有超类,而ClasssB,它位于继承的最底部,没有子类。因此,ClassA是NSObject的子类,而ClassB是ClassA的子类,也是NSObject的子类(从学术上讲,它是子类的子类或者孙类)。同样,NSObject是ClassA的超类,也是ClassB的超类。因为ClassB位于NSObject层次结构的下方,所以NSObject也是ClassB的超类。
下面是ClassB的完整声明,ClassB定义了一个名为printVar的方法:
@interface ClassB:ClassA
-(void)printVar;
@end
@implementation ClassB
-(void)printVar
{
NSLog(@“x=%i”,x);
}
@end
虽然在ClassB中没有定义任何实例变量,但可以通过printVar方法输出实例变量x的值。这是由于ClassB是ClassA的子类,因此它继承ClassA的所有实例变量(在这个例子中,实例变量只有一个)。如图8-3中所示。
图 8-3 继承实例变量和方法
(当然,图8-3并没有显示从NSObject类所继承的任何方法或实例变量,虽然NSObject有几个方法及实例变量。)
通过把它集中在一个完整的程序例子中,看看它的工作方式。为了简洁起见,将所有类声名和定义放在单个文件中(参见代码清单8-1)。
代码清单8-1
//Simple example to illustrate inheritance
import<Foundation/Foundation.h>
//ClassA declaration and definition
@interface ClassA:NSObject
{
int x;
}
-(void)initVar;
@end
@implementation ClassA
-(void)initVar
{
x=100;
}
@end
//Class B declaration and definition
@interface ClassB:ClassA
-(void)printVar;
@end
@implementation ClassB
-(void)printVar
{
NSLog(@“x=%i”,x);
}
@end
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
ClassB*b=[[ClassB alloc]init];
[b initVar];//will use inherited method
[b printVar];//reveal value of x;
[b release];
[pool drain];
return 0;
}
代码清单8-1输出结果
x=100
首先,定义b为一个ClassB对象。在分配空间并初始化b后,发送一条消息,向它应用initvar方法。但是回过头来看看ClassB的定义,就会发现从未定义过这样的方法。事实上,initVar定义在ClassA中,由于ClassA是ClassB的父类,所以ClassB可以使用ClassA的所有方法。对于ClassB而言,initVar是继承来的方法。
注意这里只简短地提到,但是alloc和init是你用过的从未在类中定义过的方法。这是因为你利用了它们都是继承方法的事实。
向b发送initVar消息之后,调用printVar方法显示实例变量x的值。输出结果x=100证实printVar能够访问这个实例变量。这是因为和initVar方法一样,这个变量也是继承的。
记住,继承的概念作用于整个继承链。因此,如果如下所示定义一个父类是ClassB的新类ClassC:
@interface ClassC:ClassB;
……
@end
那么ClassC将继承ClassB的所有方法和实例变量,同时也依次继承ClassA的所有方法和实例变量,它依次集成NS Object的所有方法和实例变量。
一定要理解以下的事实:类的每个实例都拥有自己的实例变量,即使这些实例变量是继承来的。因此,对象ClassC与对象ClassB具有完全不同的实例变量。
找出正确的方法
向对象发送消息时,你可能想知道如何选择正确的方法来应用到该对象。规则其实很简单。首先,检查该对象所属的类,以查看在该类中是否明确定义了一个具有指定名称的方法。如果有,就使用这个方法。如果这里没有定义,就检查它的父类。如果父类中有定义,就用这个方法。否则,将继续寻找。
直到发现下面两种情况中的一种,才会检查父类:发现包含指定的方法的类,或者一直搜索到根类也没有发现任何方法。如果是第一种情况,就会停止查找;如果是第二种情况,说明存在问题,就会生成类似下面的警告消息:
warning:‘ClassB’may not respond to‘-inity’
在这个例子中,你无意中向类ClassB传递了一条名为inity的消息。编译器告知你:该类型的类不能响应这种方法。同样,这是在检查ClassB的方法及直到根类(在此例中,根类为NSObject)的父类的方法之后确定的。
在某些情况下,如果没有发现该方法不会生成消息。这涉及所谓的传递的概念,这将在第9章“多态、动态类型和动态绑定”中简要地讨论。