早在PHP 3版本中,PHP就支持面向对象的编程(OOP)。虽然当时可以用面向对象编程,但是PHP对其的支持是非常简单的,而且到PHP 4时也没有得到大幅度的改进,这主要是考虑到向后兼容性的问题。后来因为广泛地提高了对OOP支持的要求,PHP 5才对整个面向对象的模型重新进行了设计,增加了大量的特性并且更改了“对象”本身的基础运行机制。
如果你是刚刚接触PHP的,本章节将为你描述整个面向对象的模型。就算对PHP 4很熟悉,你也应该阅读本章,因为几乎所有OOP的内容都在PHP 5中被改变了。
在你阅读完本章后,你将学到
OO模型的基础。
对象的创建和生命周期及如何去控制。
三种主要的访问限制关键字(public,protected和private)。
使用类继承的好处。
成功处理异常的小技巧。
3.2 对象 Objects
OOP与面向功能的编程最大的区别就是数据和代码是绑定到一个实体中的,这个实体就叫做对象。面向对象的应用通常会被切割为许多对象,而且对象之间彼此交互。每一个对象一般都是一个问题的实体,它是独立的而且拥有许多属性和方法。属性就是对象的数据,它其实就是属于对象的变量。至于方法——如果你有面向功能的编程的背景——其实就是对象支持的函数。进一步说,在交互中访问并且使用别的对象的功能叫做对象的接口。
图3.1描述了一个类。类就是对象的一个模板而且描述这个类型的对象将拥有的方法和属性。在这个例子中,该类描述的是一个人。对于你的应用中的每一个人,你都可以创建这个类的一个单独对象来描述他的信息。例如,如果你的应用中有两个人,叫做Joe和Judy,我们将创建这个类的两个单独的实例,而且调用setName()方法,通过他们的名字来初始化存储用户名的变量,$name。其他交互对象使用的方法和成员就是一个类的约定。在这个例子中,一个人与外界的约定就是两个set和get方法,setName()和getName()。
图3.1 类Person的示意图
下面的PHP代码定义了一个类,并且创建了该类的两个实例,设置了每一个实例,而且为每一个实例设置了适当的名字,然后打印出名字:
[php]class Person {
private $name;
function setName($name)
{
$this->name = $name;
}
function getName()
{
return $this->name;
}
};
$judy = new Person();
$judy->setName("Judy");
$joe = new Person();
$joe->setName("Joe");
print $judy->getName() . "\n";
print $joe->getName(). "\n";[/php]
3.3 声明一个类 Declaring A Class
你也许已经从前面的例子注意到了声明一个类(一个对象模版)是很简单的。你使用class关键字,提供一个类的名字,然后列出这个类的实例应该具备的方法和属性:
class MyClass {
... //方法的列表
...
... //属性的列表
...
}
你也许已经注意到了,在上一节的例子中我们使用private关键字声明$name属性。我们将在后面详细介绍这个关键字,它基本上意味着只有类里面的方法才能访问$name。这么做是为了迫使那些想要获取/设置这个属性的人去使用getName()和setName()方法,也就是别的对象或者源代码访问这个对象的接口。
3.4 new关键字和构造函数 The new Keyword And Constructors
类的实例是使用new关键字创建的。在上一节的例子里,我们用$judy = new Person();创建了Person类的一个实例。当你调用new的时候PHP会分派一个新的对象,并且从你定义的类中拷贝属性和方法,然后如果你有定义对象的构造函数的话,它将被自动调用。构造函数是一个命名为__construct()的方法,它在new关键字创建一个对象后自动运行。构造函数通常被用来自动地执行许多初始化操作,例如属性初始化。构造函数也接收参数,这种情况下,编写new语句的时候,你还需要给构造函数的括号内传递函数的参数。
在PHP 4中不使用__construct()作为构造函数的名字,你必须用类的名字定义一个方法,就像在C++中一样。当然这个在PHP 5中也是可行的,但是你最好在新的应用中使用新的统一的构造函数命名方式。
我们可以修改上一节的例子,并在new语句中传递人名给构造函数:
[php]class Person {
function __construct($name)
{
$this->name = $name;
}
function getName()
{
return $this->name;
}
private $name;
};
$judy = new Person("Judy") . "\n";
$joe = new Person("Joe") . "\n";
print $judy->getName();
print $joe->getName();[/php]
这个代码生成的结果与前一个例子是一样的。
注意:因为一个构造函数不能返回值,所以从构造函数内产生一个错误最常用的做法就是抛出一个异常。
3.5 析构函数 Destructors
析构函数与构造函数的作用是相反的。它是在对象被注销的时候调用的(例如,没有任何变量引用对象)。因为PHP会在请求结束后确保所有的资源都得到释放,所以析构函数显得并不是很重要。但是,它们在执行一些事件时还是很有用的,例如清空一个资源或者在对象注销的时候记录日志信息。有两种情况会调用析构函数:在执行你的脚本的时候,当一个对象所有的引用都被注销,或者当脚本运行完毕而PHP结束请求。第二种方式比较巧妙,因为第一种方法中你依赖的一些对象可能已经调用析构函数了,所以它们已经无法访问了。所以,第一种方法要慎用,而且不要在你的析构函数中引用其他对象。
定义一个析构函数非常简单,就是在类中增加一个名字为__destruct()的方法:
[php]class MyClass {
function __destruct()
{
print "An object of type MyClass is being destroyed\n";
}
}
$obj = new MyClass();
$obj = NULL;[/php]
这个脚本打印出
An object of type MyClass is being destroyed
在这个例子中,当$obj = NULL;被执行的时候,该对象的唯一一个句柄会被注销,而且调用析构函数后对象本身就被注销了。但是就算没有最后一行代码,当请求结束,执行引擎关闭的时候析构函数也会被调用的。
注意:PHP并不保证析构函数被调用的准确时间点,所以它可能在最后一个引用的对象被释放后几行语句时才调用。因此,你需要注意这个问题以免在编写你的应用的时候发生意外。
3.6 使用$this变量访问方法和属性 Accessing Methods And Properties Using The $this Variables
在对象的方法执行的时候,PHP会自动定义一个叫$this的特殊变量,它表示一个对对象本身的引用。通过使用这个变量和->符号,你可以引用该对象其他的方法和属性。例如,你可以通过使用$this->name访问$name属性(注意别在属性的名字前面加$符号)。你也可以用这种方法访问对象中的方法;例如,在一个person对象中,你可以通过$this->getName()访问getName()方法。
3.6.1 public、protected和private属性 public, protected, and private Properties
OOP中的一个关键范例就是封装和对象属性(也可以叫做成员变量)的访问保护。大部分通用的OO语言都有三种主要的访问限制关键字:public、protected 和 private。在类定义里定义一个类成员的时候,开发者需要设置这三个修饰符的其中一种,然后再声明成员本身。如果你熟悉PHP 3或者PHP 4的对象模型的话,那里所有的类的成员都用var关键字来定义,这样的声明方式就像PHP 5中的public。但是var还是被保留下来了,为了保持向后的兼容性。不过我们不赞成这么写,而且建议你把脚本中的var转变成新的关键字:
[php]class MyClass {
public $publicMember = "Public member";
protected $protectedMember = "Protected member";
private $privateMember = "Private member";
function myMethod(){
// ...
}
}
$obj = new MyClass();[/php]
这个例子向大家示范三种修饰符的使用。
首先,大家来了解下面每一个访问修饰符的详细定义:
public。公共成员既可以通过对象外部使用$obj->publicMember访问,也可以用特殊变量$this(例如,$this->publicMember)从内部的方法访问。如果另外一个类继承了这个公共变量,这个规则同样适用,而且从这个类的对象的外部和内部的方法都可以访问。
protected。保护成员只能从对象内部的方法访问—例如,$this->protected- Member。如果另外一个类继承一个保护的成员,同样的规则也适用,而且它可以从继承类实例化的对象的方法中通过特殊的$this变量访问到。
private。与私有成员与保护成员类似,因为它们都只能从对象内部的方法访问。但是,私有成员不能从继承类实例化的对象的方法访问。因为私有属性在继承的类中是看不到的,而且两个相关的类可以分别声明一个名字相同的私有变量。也就是两个类都只能看到自己的私有属性,私有成员之间是没有关系的。
通常,你会用public声明成员以便从对象域(例如它的方法)外面访问到该成员,而用private声明成员为只允许对象内部逻辑访问。或者使用protected声明变量为只允许对象内部逻辑访问,但从继承类中可以访问甚至修改它的值:
[php]class MyDbConnectionClass {
public $queryResult;
protected $dbHostname = "localhost";
private $connectionHandle;
// ...
}
class MyFooDotComDbConnectionClass extends MyDbConnectionClass {
protected $dbHostname = "foo.com";
}[/php]
这个不完整的例子象征性地显示了每一种访问修饰符的使用方法。这个类处理一个数据库的连接,包括访问数据库查询:
数据库连接的句柄保存在一个private成员中,因为它是用来给类的内部逻辑使用的而且不能被这个类的使用者访问。
在这个例子中,数据库的主机名不能被MyDbConnectionClass类的使用者看到。如果想修改它,开发者需要从这个初始类中继承一个新的类然后修改主机名。
数据库查询本身是可以让开发者访问到的,所以它被声明为一个公共变量。
请注意,设置了访问修饰符后,类(或者更加具体点,它们开放给外界的接口)继承时总是保持一种“是一”的关系。因此,如果一个父类把一个成员声明为公共成员,继承的子类也必须把它声明为公共成员。否则,子类将不能与父类保持“是一”的关系,这种关系意味着你对父类做的任何修改也会影响子类。
3.6.2 public、protected和private方法 public, protected, and private Methods
访问修饰符也可以用来修饰对象的方法,而且规则是一样的:
public 方法可以在任何作用域访问到。
protected 方法只能从类或者继承类的一个成员中访问到。
private 方法只能从类的一个成员中访问到,而且无法从继承类的成员中访问到。就跟属性一样,private方法可以在继承类中重新定义。每一个类只能看到它自己定义的私有方法:
[php]class MyDbConnectionClass {
public function connect()
{
$conn = $this->createDbConnection();
$this->setDbConnection($conn);
return $conn;
}
protected function createDbConnection()
{
return mysql_connect("localhost");
}
private function setDbConnection($conn)
{
$this->dbConnection = $conn;
}
private $dbConnection;
}
class MyFooDotComDbConnectionClass extends MyDbConnectionClass {
protected function createDbConnection()
{
return mysql_connect("foo.com");
}
}[/php]
这个包含纲要代码的例子可以被用来作为一个数据库连接的类。connect()方法是可以从外部代码访问的。createDbConnection()方法是一个内部的方法,而且可以让你继承类的时候更改它;所以,它被标识为protected。setDbConnection()方法是一个完全面向类内部的方法,所以标识为private。
注意:如果没有给一个方法设置访问修饰符,会被默认为public修饰符。为此,在接下来的章节中,public经常被省略。
3.6.3 静态属性 Static Properties
现在你了解到,类可以声明属性。每一个类的实例(例如,对象)拥有它自己的属性拷贝。但是一个类也可以包含静态属性。和常规属性不一样的是,这些属性属于类本身而不属于类的任何实例。因此,它们经常被叫做类属性以便和对象或者实例的属性区分开来。你也可以把静态属性认为是存储在类当中的全局变量,而且你可以在任何地方通过类访问它们。
静态属性是用static关键字定义的:
[php]class MyClass {
static $myStaticVariable;
static $myInitializedStaticVariable = 0;
}
[/php]要访问一个静态变量时,你必须用属性所在的类的名字来限制它
MyClass::$myInitializedStaticVariable++;
print MyClass::$myInitializedStaticVariable;
这个例子打印出数字1。
如果你想在类里面的方法中访问静态成员,你还可以通过在成员的名字前面加上特殊的类名self来访问它,self是一个方法所属的类的缩写:
[php]class MyClass {
static $myInitializedStaticVariable = 0;
function myMethod()
{
print self::$myInitializedStaticVariable;
}
}
$obj = new MyClass();
$obj->myMethod();[/php]
这个例子打印出数字0。
你可能在思考,这样的整体静态处理是否真的有用。
让我们看一下,通过它如何把一个唯一的ID传递到类的所有实例中:
[php]class MyUniqueIdClass {
static $idCounter = 0;
public $uniqueId;
function __construct()
{
self::$idCounter++;
$this->uniqueId = self::$idCounter;
}
}
$obj1 = new MyUniqueIdClass();
print $obj1->uniqueId . "\n";
$obj2 = new MyUniqueIdClass();
print $obj2->uniqueId . "\n";[/php]
这个例子打印出
1
2
第一个对象的$uniqueId属性等于1而第二个对象的等于2。
还有一个更好的例子是在单件模式中使用静态变量,我们会在下一章节中示范。
3.6.4 静态方法 Static Methods
与静态属性相似,PHP支持把方法声明为静态的。也就是说静态方法是类的一部分而且不被限制到任何一个特定的对象实例和类的属性。因此,$this在这些方法中将不能使用,但是类本身可以通过self访问。因为静态方法不被限制到任何特定的对象,所以你可以不创建对象实例就通过class_name::method()语法调用它。你还可以在一个对象实例中通过$this->method()访问这些静态方法,但是在这些方法中$this是没有定义的。为了更加清楚些,你应该使用self::method()而不是$this->method()。
这里有个例子:
[php]class PrettyPrinter {
static function printHelloWorld()
{
print "Hello, World";
self::printNewline();
}
static function printNewline()
{
print "\n";
}
}
PrettyPrinter::printHelloWorld();
[/php]
这个例子将打印字符串"Hello World"和一个换行符。虽然这个例子看起来不是很实用,但是你可以看到printHelloWorld()可以被类调用,不是通过创建一个对象实例而只是用类的名字,而且静态方法本身还可以通过self::符号调用另一个类的静态方法printNewline()。你还可以在子类中通过parent::符号调用一个父类的方法,这种用法会在本章的后续部分讲解。
3.7 类的常量 Class Constants
全局的常量已经在PHP中存在很长时间了。它们可以用define()函数定义,这个在第2章“PHP 5基础语言”就已经讲过了。因为PHP 5中对类的封装进行了改进,你现在可以在类中定义常量。与静态成员类似,它们属于类本身而不是类的实例。类的常量总是对大小写敏感的。定义它们的语法很直观,而且访问类中的常量跟访问静态成员是类似的:
[php]class MyColorEnumClass {
const RED = "Red";
const GREEN = "Green";
const BLUE = "Blue";
function printBlue()
{
print self::BLUE;
}
}
print MyColorEnumClass::RED;
$obj = new MyColorEnumClass();
$obj->printBlue();[/php]
这个代码打印出"Red"和"Blue"。这表示访问常量的方法包括从类里面的方法通过self关键字访问和通过类的名字"MyColorEnumClass"访问两种。
就像它们的名字暗示的,常量的值是不变的,一旦定义后不能被改变也不能被注销。常量最常用的地方是定义枚举元素,例如前面的例子,或者定义一些配置数据例如数据库用户名。用常量的原因是你不想应用中其他情况下别人可以改变它们。
注意:对于全局的常量来说,你最好用大写字母来编写常量的名字,因为这是一个习惯用法。
3.8 克隆对象 Cloning Objects
在创建一个对象的时候(使用new关键字),返回的值是一个指向对象的句柄,或者换言之,是对象的ID号。这跟PHP 4是不一样的,在PHP 4中返回的值就是对象本身。这并不意味着调用方法或者访问属性的语法被改变了,只是对象在复制的时候意义被更改了。
分析下面的代码:
[php]class MyClass {
public $var = 1;
}
$obj1 = new MyClass();
$obj2 = $obj1;
$obj2->var = 2;
print $obj1->var;[/php]
在PHP 4中,这个代码会打印出1,因为$obj2被赋予了$obj1的对象值,因此$obj2是$obj1的一个拷贝,从而改变$obj2的时候$obj1不会被更改。但是在PHP 5中,因为$obj1是一个对象句柄(它的id号),所以复制到$obj2的是一个句柄。因此,当改变$obj2的值的时候,你其实已经改变了$obj1指向的同一个对象。因此,运行这个代码片断后,结果是打印出2。
可是在有些时候, 你确实需要复制一个对象的拷贝。如何才能实现呢?解决办法是语言命令clone。这个内置的操作符会自动创建一个新的对象实例,并且附带原对象的所有属性。对象的属性的值也会被原样复制。另外,你可以定义一个__clone()方法来执行任何最后的操作,它在新创建的对象中被调用。
注意:引用是可以作为引用复制的,而且不会执行更加深入的复制。这意味着如果类中一个属性是通过引用指向另一个变量(通过引用赋值后)的话,在自动克隆执行后,克隆出来的对象的该属性也会指向相同的变量。
更改上一个例子中的$obj2 = $obj1;为$obj2 =clone $obj1;将会把$obj1对象新的拷贝的句柄赋值给$obj2,运行结果是1。
就像先前提到的,在你的任何一个类中,可以执行一个__clone()方法。在新的(克隆的)对象被创建后,你的__clone()方法会被调用而且在里面可以用$this变量访问克隆的对象。
下面的例子描述一个你可能要执行__clone()方法的一个典型的情况。假设你有一个对象,它存储着一个资源,例如一个文件句柄。你需要一个新的对象,而且它不能指向同一个文件句柄,而是打开一个新的文件从而拥有它自己的一个拷贝:
[php]class MyFile {
function setFileName($file_name)
{
$this->file_name = $file_name;
}
function openFileForReading()
{
$this->file_handle = fopen($this->file_name, "r");
}
function __clone()
{
if ($this->file_handle) {
$this->file_handle = fopen($this->file_name, "r");
}
}
private $file_name;
private $file_handle = NULL;
}[/php]
虽然这个代码只写了一部分,但是你可以看到如何控制克隆过程。在这个代码片断中,$file_name被原样地从原对象中复制了,但是原对象有一个打开的文件句柄(它也被拷贝到克隆的对象),因此新的对象拷贝将通过打开该文件而创建一个它自己的指向该文件的句柄。
3.9 多态 Polymorphism
多态这个课题也许是OOP中最重要的部分了。OOP不是把函数和数据简单地集合起来,而是使用类和继承轻松地描述现实生活中的情况。它们还可以通过继承复用代码而轻松升级项目。另外,为了编写健壮的可扩展的代码,你通常需要尽量减少使用流程控制语句(例如if()语句)。多态将满足所有这些甚至更多的需求。
分析下面的代码:
[php]class Cat {
function miau()
{
print "miau";
}
}
class Dog {
function wuff()
{
print "wuff";
}
}
function printTheRightSound($obj)
{
if ($obj instanceof Cat) {
$obj->miau();
} else if ($obj instanceof Dog) {
$obj->wuff();
} else {
print "Error: Passed wrong kind of object";
}
print "\n";
}
printTheRightSound(new Cat());
printTheRightSound(new Dog());[/php]
结果是
miau
wuff
可以很容易地看出这个例子是不可扩展的。如果想通过增加另外三种动物的声音来扩展的话,你可能不得不在printTheRightSound()函数中增加另外三个else if模块以便检查你的对象是其中哪一种新动物的实例,而且接下来还要增加代码来调用每一种发声的方法。
多态使用继承来解决这个问题。它可以让你继承一个父类,继承所有父类的方法和属性然后创建“是一”关系。
按照前一个例子,我们将创建一个新的类,名叫Animal。然后所有其他的动物从这个父类继承,从而为每一个特定的动物种类创建“是一”关系,例如Dog的父类(或者祖先)是Animal。
继承是通过extends关键字执行的:
class Child extends Parent {
...
}
下面演示如何用继承重写先前的例子:
[php]class Animal {
function makeSound()
{
print "Error: This method should be re-implemented in the
➥children";
}
}
class Cat extends Animal {
function makeSound()
{
print "miau";
}
}
class Dog extends Animal {
function makeSound()
{
print "wuff";
}
}
function printTheRightSound($obj)
{
if ($obj instanceof Animal) {
$obj->makeSound();
} else {
print "Error: Passed wrong kind of object";
}
print "\n";
}
printTheRightSound(new Cat());
printTheRightSound(new Dog());[/php]
结果是
miau
wuff
你可能发现无论往这个例子中增加多少种动物类型,都不需要对printTheRightSound()函数做任何更改,因为instanceof Animal可以检测所有这些类型,而且$obj->makeSound()调用也是如此。
还可以改进这个例子。你可以使用PHP的一些修饰符更好地控制继承过程。它们会在本章的后续部分陆续讲解。例如,类Animal和它的方法makesound()可以标识为abstract,这不仅意味着你不需要在Animal类的makesound()方法的定义中提供一些无意义的函数执行体,
而且还可以强制由任何子类来实现它。另外,我们可以给makesound()设定访问修饰符,例如公共修饰符,表示它可以在你的代码的任何地方调用。
注意:PHP不像C++那样支持多重继承。它提供了另外一个方法来为一个类创建一个以上的“是一”关系,该方法是通过类似Java的接口来实现的,我们会在本章节的后续部分讲到。
3.10 parent::和self:: parent:: And self::
PHP支持两个类名的保留字让你在编写OO应用的时候可以轻松调用。self::指向当前的类,而且它通常用来访问静态成员、方法和常量。parent::指向父类,而且它经常被用来调用父类的构造函数和方法,它也可以用来访问父类的成员和常量。你应该用parent::而不是父类的某个具体名字,这是为了让你可以方便地更改你的类的层次,因为你不用固定写入某个父类的名字。
下面的例子同时使用了parent::和self::来访问Child类和Ancestor类:
[php]class Ancestor {
const NAME = "Ancestor";
function __construct()
{
print "In " . self::NAME . " constructor\n";
}
}
class Child extends Ancestor {
const NAME = "Child";
function __construct()
{
parent::__construct();
print "In " . self::NAME . " constructor\n";
}
}
$obj = new Child();[/php]
上一个例子输出
In Ancestor constructor
In Child constructor
在任何需要的时候,确保你尽可能使用这两个类名。
3.11 instanceof运算符 instanceof Operator
PHP增加instanceof作为更符合使用习惯的运算符,并代替已有的is_a()内置函数(现在不推荐使用)。与后者不一样的是,instanceof被当成一个逻辑二元运算符来使用:
[php]class Rectangle {
public $name = __CLASS__;
}
class Square extends Rectangle {
public $name = __CLASS__;
}
class Circle {
public $name = __CLASS__;
}
function checkIfRectangle($shape)
{
if ($shape instanceof Rectangle) {
print $shape->name;
print " is a rectangle\n";
}
}
checkIfRectangle(new Square());
checkIfRectangle(new Circle());[/php]
这个小程序打印出'Square is a rectangle\n'。注意__CLASS__的使用,它是一个特殊的常量,用来存储当前类的名字。
就像先前提到的,instanceof是一个运算符,因此它可以存在于表达式中并连接其他运算符(例如,![否定]运算符)。这可以让你轻松编写一个checkIfNotRectangle()函数:
[php]function checkIfNotRectangle($shape)
{
if (!($shape instanceof Rectangle)) {
print $shape->name;
print " is not a rectangle\n";
}
}[/php]
注意:instanceof还检查一个对象是否执行了一个接口(这个也是一个经典的“是一”关系)。接口将在本章后续部分讲述。
3.12 Abstract方法和类 Abstract Methods And Class
在设计类的层次时,你可能会想部分地保留一些方法给继承类执行。假设你的类层次如图3.2所示:
图3.2 类的分层
其中Shape类实现setCenter($x, $y)是有意义的,同时把draw()方法留到具体的Square和Circle类中执行。为此你可能需要声明draw()方法为一个abstract方法,这样PHP知道你是有意地不让它在Shape类中实现。同时,Shape类可以被叫做abstract类,这意味着它不是一个具备完整功能的类而且只为了被继承。你是不能实例化一个abstract类的。而且你可以定义任意个方法为abstract,但是只要有一个方法被定义为abstract,这整个类也就需要声明为abstract。这种双重的定义可以让你选择定义一个类为abstract,就算它没有任何abstract方法,而且这种定义还可以促使你定义一个拥有abstract方法的类为abstract,以便别人清楚地了解你的想法。
图3.2的类图可以转换成如下相应的PHP代码:
[php]abstract class Shape {
function setCenter($x, $y) {
$this->x = $x;
$this->y = $y;
}
abstract function draw();
protected $x, $y;
}
class Square extends Shape {
function draw()
{
// 这里到达了绘制方形的代码
...
}
}
class Circle extends Shape {
function draw()
{
//这里到达了绘制圆形的代码
...
}
}[/php]
你可以看到draw()抽象方法没有包含任何代码。
注意:与其他一些语言不同的是,你不能给抽象的方法定义一个默认的执行体。但是在PHP中,一个方法可以是abstract(没有代码)的,或者可以是完整定义的。
3.13 接口 Interfaces
类的继承让你可以描述几个类之间的父与子的关系。例如,你可以使用一个基类Shape,然后让Square和Circle类继承Shape。但是,你可能会经常需要增加额外的“interfaces”到类中,基本上来说是类需要附带额外的约定。这个功能在C++中可以用多重继承来实现,如同时从两个类继承。PHP选择了接口而不是多重继承,接口可以让你指定类需要附带的额外的约定。声明一个接口与声明一个类是类似的,但是它只包含函数原型(没有执行体)和常量。任何一个“implements”这个接口的类将自动获得这个接口定义的常量并且,作为实现的类需要给接口的函数原型提供函数定义(除非你定义实现接口的类为abstract), 接口的函数都是abstract的。
通过下面的语法来实现一个接口:
class A implements B, C, ... {
...
}
实现了一个接口的类都将与该接口拥有一个instanceof(“是一”)关系;例如,如果类A执行接口myInterface,下面的代码将打印出'$obj is-A myInterface':
$obj = new A();
if ($obj instanceof myInterface) {
print '$obj is-A myInterface';
}
下面的例子定义了名叫Loggable的接口,它让其他类来实现并通过MyLog()定义需要记录哪些信息的日志。那些没有实现这个接口的类的对象传递给MyLog()函数将会打印出错误的信息:
[php]interface Loggable {
function logString();
}
class Person implements Loggable {
private $name, $address, $idNumber, $age;
function logString() {
return "class Person: name = $this->name, ID = $this
➥>idNumber\n";
}
}
class Product implements Loggable {
private $name, $price, $expiryDate;
function logString() {
return "class Product: name = $this->name, price = $this
➥>price\n";
}
}
function MyLog($obj) {
if ($obj instanceof Loggable) {
print $obj->logString();
} else {
print "Error: Object doesn’t support Loggable interface\n";
}
}
$person = new Person();
// ...
$product = new Product();
MyLog($person);
MyLog($product);[/php]
注意:接口总是被认为是public的;因此,你不能在定义接口时,给接口的函数原型设置访问修饰符。
注意:你不能实现互相冲突的多重接口(例如,接口如果定义相同的常量和方法)。
3.14 接口的继承 Inheritance of Interfaces
接口是可以从别的接口继承来的。继承接口的语法与继承类的语法类似,但是 接口可以允许多重继承:
interface I1 extends I2, I3, ... {
...
}
与类实现接口类似, 一个接口只能继承与自己互相不冲突的接口(也就是说如果I2定义了在I1已经定义的方法或者常量,你将收到报错信息)。
3.15 final 方法 final Methods
到目前为止,你已经看到当你扩展一个类(或者从一个类继承)时,你可以用一个新的实现体重写被继承的方法。但是,很多时候你可能需要确保一个方法不能被继承类改写。为此,PHP支持类似Java的final访问控制符来声明一个方法是不可重写的。
下面的例子不是一个合法的PHP脚本,因为它试图重写一个final方法:
[php]class MyBaseClass {
final function idGenerator()
{
return $this->id++;
}
protected $id = 0;
}
class MyConcreteClass extends MyBaseClass {
function idGenerator()
{
return $this->id += 2;
}
}
[/php]
这个脚本将不会执行,因为在MyBaseClass中把idGenerator()定义为final,它不允许继承的类重写idGenerator()并且修改id增长的逻辑。
3.16 final类 final Classes
与final方法类似,你还可以定义一个类为final。这么做将不允许其他类继承此类。下面的代码将不能运行:
final class MyBaseClass {
...
}
class MyConcreteClass extends MyBaseClass {
...
}
MyBaseClass已经被声明为final;MyConcreteClass不能继承它。因此,这个代码的执行失败。
3.17 _ _toString()方法
__toString() Method
考虑下面的代码:
[php]class Person {
function __construct($name)
{
$this->name = $name;
}
private $name;
}
$obj = new Person("Andi Gutmans");
print $obj;[/php]
它打印出如下信息:
Object id #1
与大多数其他数据类型不同的是,你通常没有兴趣打印对象的id。另外,对象经常提供应该真正打印出来的数据——例如,当你打印一个表示人的类的对象时,把该信息打印出来才是有意义的。
为此,PHP为你提供一个叫__toString()的函数,你可以用它来返回表示对象的字符串信息,而且一旦定义它,打印命令将调用它并打印出返回的字符串。
通过使用__toString(),上面的例子可以更改为更加有用的形式:
[php]class Person {
function __construct($name)
{
$this->name = $name;
}
function __toString()
{
return $this->name;
}
private $name;
}
$obj = new Person("Andi Gutmans");
print $obj;[/php]
它打印出如下信息:
Andi Gutmans
目前__toString()只能被print和echo语言结构调用。将来,它们还可能被普通的字符串运算符(例如字符串串联)调用并且直接构造为字符串。
3.18 异常处理 Exception Handling
异常处理往往是软件开发中疑问比较多的方面之一。不仅仅是因为开发者在错误出现时很难决定如何处理(例如数据库失败,网络错误,或者一个软件Bug),而且很难在代码中的所有地方都插入失败检查并调用恰当的函数来处理。一个更加复杂的任务就是在你处理错误后,你如何才能修正程序的执行流程从一个特定地方继续执行?
今天,大多数现代的语言都支持一些各自的很流行的try/catch/throw异常处理范例。try/catch是一个闭合的语言结构,它们保护被其包含的源代码而且会告诉PHP,“我正在处理发生在这些代码中的异常。”异常或者错误在被检测到的时候“抛出”并且PHP实时地去搜索它所调用的堆栈,查看是否存在一个相应的try/catch结构可以来处理这个异常。
这种方法有许多好处。首先,你不用放置if()语句在任何异常有可能出现的地方;因此可以减少很多代码。你可以把整个部分的代码都装入try/catch结构并且当一个错误发生时处理错误。另外,在你用throw语句检测到一个错误后,可以轻松地转递到代码中需要负责处理异常的地方并继续让程序执行下去,因为throw展开了函数调用的堆栈直到它检测到一个合适的try/catch块。
Try/catch的语法如下:
try {
... // 会抛出一个异常的代码
} catch (FirstExceptionClass $exception) {
... //会处理这个异常的代码
} catch (SecondExceptionClass $exception) {
}
try{}结构装入的代码可以抛出一个异常,后面可以加上一系列的catch语句,每一个catch声明哪个异常类应该调用并且在catch块中异常的对象应该用什么变量名来访问。
当一个异常被抛出后,首先到达第一个catch()并且执行instanceof比较,判断抛出的异常是否是catch()中包含的异常类的实例。如果结果是true,PHP进入catch块并且通过声明的变量名可以访问该异常。如果结果是false,PHP将检查下一个catch语句。一旦进入一个catch语句,后面的catch语句将不再进入,就算instanceof判断也是ture。如果找不到任何一个相关的catch语句,PHP引擎检查同一个函数里外部加上的try/catch语句。如果没有一个存在,PHP通过展开调用堆栈到调用的函数继续搜索。
throw语句:
throw <object>;
只能抛出对象。你不能让它抛出任何其他基础数据类型,例如字符串或者整型值。PHP中存在一个预定义的异常类名叫Exception,你自己所有的异常处理类都必须从它继承。如果你尝试抛出一个不是由Exception类继承的类,最后将得到一个运行错误。
下面的代码片断展示了这个内置的异常处理类的接口(构造函数声明中的方括号表示可选的参数,并不是标准的PHP语法):
[php]
class Exception {
function __construct([$message [,$code]]);
final public getMessage();
final public getCode();
final public getFile();
final public getLine();
final public getTrace();
final public getTraceAsString();
protected $message;
protected $code;
protected $file;
protected $line;
}[/php]
下面是一个异常处理的完整例子:
[php]class NullHandleException extends Exception {
function __construct($message)
{
parent::__construct($message);
}
}
function printObject($obj)
{
if ($obj == NULL) {
throw new NullHandleException("printObject received NULL
➥object");
}
print $obj . "\n";
}
class MyName {
function __construct($name)
{
$this->name = $name;
}
function __toString()
{
return $this->name;
}
private $name;
}
try {
printObject(new MyName("Bill"));
printObject(NULL);
printObject(new MyName("Jane"));
} catch (NullHandleException $exception) {
print $exception->getMessage();
print " in file " . $exception->getFile();
print " on line " . $exception->getLine() . "\n";
} catch (Exception $exception) {
//这里将不会被执行
}[/php]
运行这个脚本打印出
Bill
printObject received NULL object in file
C:\projects\PHP 5\tests\test.php on line
12
请注意Jane这个名字是没有被打印出来的,只打印了Bill。这是因为printObject(NULL)语句在函数中抛出了一个异常,因此Jane被忽略了。在获取处理器中,被继承的方法例如getFile()被用来在异常出现时提供附加的信息。
提示:你可能注意到了NullHandleException的构造函数调用了其父类的构造函数。如果NullHandleException的构造函数忘记定义了,默认情况下new将调用父类的构造函数。但是良好的编程习惯是增加一个构造函数并明确调用父类的构造函数,所以如果你突然决定增加一个自己的构造函数时不要忘记这么操作。
目前,为了考虑与PHP 4的向后兼容,许多内置的方法并不会抛出异常。这样做在一定程度上限制了异常的使用,但是它允许你在自己的代码使用它们。一些PHP 5中的新扩展——主要是面向对象的扩展——倒是会抛出异常。你可以查看扩展的文档并确保得到明确的信息。
提示:在使用扩展的时候,请遵循这些基本的规则(为了提高性能和规范代码编写):
1. 记住异常就是异常。你应该只使用它们来处理问题,因此引来下一个规则……
2. 永远不要使用异常来控制流程。因为这样会让你的代码很难理解(类似其他语言中的goto),而且会让你的代码运行缓慢。
3. 异常应该只包含特定的错误信息,而且不应包含会影响获取处理器中流程控制和逻辑的参数(或者附加的信息)。
3.19 __autoload()
这里是一个使用__autoload()的经典例子:
MyClass.php:
[php]<?php
class MyClass {
function printHelloWorld()
{
print "Hello, World\n";
}
}
?>
general.inc:
<?php
function __autoload($class_name)
{
require_once($ _SERVER["DOCUMENT_ROOT"] . "/classes/
➥$class_name.php");
}
?>
main.php:
<?php
require_once "general.inc";
$obj = new MyClass();
$obj->printHelloWorld();
?>[/php]
注意:这个例子并没有忽略PHP的开始和结束标记(就像第2章中展示的其他例子一样),因为它被分割散布到不止一个文件当中,因此并不是一个代码片断。
只要MyClass.php存在于Web服务器的根目录下面的classes/目录中,代码将打印
Hello, World
我们了解到MyClass.php并没于真正包含于main.php文件中而是暗中通过调用__autoload()函数包含进来的。你通常可以将__autoload()函数定义在一个文件中,并在你所有的主脚本文件包含它(与例子中的general.inc类似),从而当你需要增加调用的类的数量时,可节省大量的代码而且可以减少维护代码的工作量。
注意: 虽然PHP中类名字是大小写不敏感的,但是类的名字发送给__autoload()函数时是区分大小写的。如果你更喜欢让你的类的文件名对大小写敏感,请确保在你的代码中保持一致性,而且你的类中也要使用正确大小写的名字。而如果你不希望这么做,你 可以使用strtolower()函数把类的名字转变为小写再尝试包含它,同时保存类的文件名都用小写。
3.20 在函数参数中提示类的类别 Class Type Hints in Function Parameters
这里有一个PHP函数的典型例子,该函数接收一个函数参数并且首先检查它是否属于它要求的类:
function onlyWantMyClassObjects($obj)
{
if (!($obj instanceof MyClass)) {
die("Only objects of type MyClass can be sent to this
function");
}
...
}
在每一个相关的函数中都写这样一个对象类型的建议需要大量的工作。为了节省你的时间,PHP可以让你在函数的参数前面设置参数的类别。
通过类的类别提示,相同的例子可以写成如下方式:
function onlyWantMyClassObjects(MyClass $obj)
{
// ...
}
当这个函数被调用时,PHP会在执行函数的代码前自动执行一个instanceof检查。如果检查失败,它中断运行并打印一个错误。因为检查使用的是instanceof检查,所以发送任何一个满足与类的类别是“是一”关系的对象都是合法的。这个特性在开发的时候非常有用,因为它帮助你确保把只属于函数处理类型的对象传递给该函数。
3.21 总结 Summary
本章讲述了PHP 5对象模型,包括类和对象的概念、多态和其他重要的面向对象的概念和语义。如果你刚刚接触PHP,但是曾经用过别的面向对象的语言,你可能不能理解为什么人们到现在才可以编写面向对象的代码。如果你曾经使用过PHP 4中面向对象的代码,你现在可能会在为这些新特性而欢快不已。