在类的“继承”关系中,父类与子类是两个最基本的概念,我们先来看一下生活中继承相关的概念。如下图。
本人非汽车专业人士,只为说明软件问题,不能作为科学依据哈!:)
在图中,我们可以看到,单讲“汽车”概念包括一些基本的特点,如都有动力系统、车门、轮子等。再看更具体一点的汽车类型,如轿车就比较传统,包括很多综合应用特性;SUV则突出高大的外形和实用的空间;跑车要有出色的动力和操控;卡车则是主要是为了运输。重要的是,这些车辆类型都有着“汽车”的基本特征。
那么,从面向对象编程的角度上看,我们就应该将“汽车”定义为一个基本的类,然后,可以通过继承来创建更具体的汽车类型;此时,“汽车”就是一个父类,而轿车、SUV、跑车、卡车等就是“汽车”的子类。
父类定义了一组共同的属性和方法,这些成员定义了子类的基本结构;子类则通过“继承”父类,获取基本的属性和方法,然后,子类在父类的基础之上,可以进行对父类的成员进行扩展或修改,以定义更加具体的属性和方法。
此外,在一些编程语言中,父类也称为基类。
在PHP中,我们定义两个类的继承关系,关键是在子类定义时使用extends关键字。extends的意思为扩展,但在传统的面向对象编程概念中,子类与父类的关系则称为继承(inherit),这只是名词上的区别,但应用上的本质是一样的。 如下面的代码,我们演示了基本的继承应用。
class CAuto
{
public doorCount = 4; publicwheelCount = 4;
public maxSpeed = 0; public function drive() { echo "The Auto is running, Max Speed ",this->maxSpeed," km/h.";
}
}
class CCar extends CAuto
{}
car = new CCar();car->drive();
运行代码会显示“The Auto is running, Max Speed 0 km/h. ”,这是因为CCar类通过继承CAuto类默认拥有了它的所有成员,也就是说,CCar类实例实际调用的就是CAuto类中的drive()方法。
不过,这不科学呀,汽车的最大速度不应该是0呀!好的,我们修改一个CCar类的定义,如下面的代码。
class CCar extends CAuto
{
public maxSpeed = 250; public function drive() { echo "The Car is running, Max Speed ",this->maxSpeed," km/h.";
}
}
再次执行代码,我们会看到“The Car is running, Max Speed 250 km/h. ”,是的,我们通过在CCar类中重写的drive()方法,实现了与父类中同名方法的不同行为。
这两个类的使用就是继承的最基本形式了,不过,关于继承还有很多主题需要我们关注,下面就一一讨论。
能否继承
前面,我们定义的CAuto类可以被CCar很愉快地继承,但是,有些类并不希望被其它的类继承的,当然,并不是这些类太小气,而是代码结构方面的设计要求所决定的,那么,对于不能继承的类,我们应该怎么定义呢?
在PHP中,我们在定义类时使用final关键字,那么,这个类就不能被继承,如下面的代码。
final class ClsA
{}
class ClsB extends ClsA
{}
代码很简单,不过,它们是不能正确运行的。在定义ClsA类时,我们使用了final关键字,它将不能被继承;然后,我们定义ClsB时继承ClsA,此时,错误就产生了。
现在,我们知道,默认情况下,类是很慷慨的,它可以把自己的成员提供给它的子类使用,但有时候,某些类是需要一些个人主义的,那么,这个类的定义就应该使用final关键字。
不过,有时候,父类可能有些纠结,在它的成员中,有些可以让子类调用,而有些则不能;那么,这些成员应该如何区分呢?答案就是在定义类成员时使用可访问性关键字,如:
- 父类中使用public或protected关键字定义的成员,可以被子类访问。
- 父类中使用private关键字定义的成员,不能被子类访问。
成员扩展
前面,我们已经知道在子类定义时使用extends关键字,可以自动拥有父类的所有公共和受保护的成员,但是,我们也说过,在子类可能需要更多的成员来完成更具体的工作,此时,我们就可以在子类创建新的属性和方法,完成对父类功能的扩展。如下面的代码:
class ClsA
{
public name = ''; public function work() { echothis->name,' is working.';
}
}
class ClsB extends ClsA
{
public speed = 0; public function run() { echothis->name,' is running, Speed is ',this->speed,'km/h.'; }
}objB = new ClsB();
objB->name = 'Tom';objB->speed = 10;
objB->work();
echo '<br>';objB->run();
代码中,我们首先定义了ClsA类,其中定义了name变量和work()方法,然后,我们创建了它的子类ClsB;在ClsB类中,我们又定义了speed变量和run()方法,这样,我们也就完成了子类对父类成员的扩展工作。运行代码结果如下图。
实际上,我们可以看到,对于类的功能扩展,实际上是非常简单的,我们只需要在子类定义新的成员就可以了。
重写父类成员
通过CAuto和CCar类的定义,我们也可以看到,在子类中,我们可以定义同名的成员,此时,子类中的新的成员就可以覆盖父类中的同名成员,再看一下下面的例子。
class ClsA
{
public name = ''; public function work() { echothis->name,' is working.';
}
}
class ClsB extends ClsA
{
public speed = 0; public function work() { echothis->name,' is running. Speed is ',this->speed,'km/h.'; }
}objB = new ClsB();
objB->name = 'Tom';objB->speed = 10;
$objB->work();
本例中的ClsA类与前面的示例是一样的,不同点在于ClsB类的定义,我们可以看到,在ClsB类中,我们定义了work()方法,此时,这个方法也就是覆盖了ClsA类中的同名方法,当我们调用ClsB实例的work()方法,结果如下图。
在子类中,如果我们使用同名成员覆盖了父类中的成员,并不意味着在子类中就不能使用父类成员了,不过,前提是父类的成员应该定义为public或protected。下面,我们修改ClsB类中的work()方法,如下面的代码。
class ClsB extends ClsA
{
public speed = 0; public function work() { parent::work(); echo '<br>'; echothis->name,' is running. Speed is ',$this->speed,'km/h.';
}
}
请注意代码中的“parent::work();”语句,我们使用parent关键字来调用父类中的成员,这样,修改后的ClsB类中的work()方法就可以显示两个信息,如下图。
与关键字parent相对应的是self关键字,它代表本类,如self::work()。
在这一部分,我们方法演示了如何在子类中重写父类成员,而变量的重写方式是相同的;我们需要注意的是,在子类中重写父类成员时,只需要在子类中定义同名的成员就可以了,如果在子类中需要访问父类中的成员(定义为public或protected),可以使用parent关键字,而调用本类成员则可以使用self关键字。此外,parent和self关键字不但可以调用实例成员,同时也可以调用静态成员。
关于重写需要注意,当父类中的方法定义为终极版,也就是使用final关键字时,也是不能被子类重写的。当父类中的成员定义为静态时,在子类中重写也必须定义为静态成员,反之亦然。
继承关系中的对象类型判断
我们知道,使用instanceof运算符或is_a()函数,可以判断一个对象是不是一个类的实例,那么,当我们定义了类的继承关系时,会有什么样的情况出现呢?我们先看下面的代码。
class ClsA
{}
class ClsB extends ClsA
{}
objB = new ClsB();
var_dump(objB instanceof ClsB); // 显示bool(true)
print '<br>';
var_dump($objB instanceof ClsA); // 显示bool(true)
我们将objB对象定义为ClsB类的实例,所以,第一个var_dump()函数会显示bool(true),这没什么问题;但是,第二个var_dump()函数中,我们使用instanceof运算符判断objB对象是否为ClsA类的实例,结果依然是true。
这实际上就是继承关系中的一个重要特点,当一个对象定义为子类的实例时,它同时也是父类的间接实例。