CSDN博客

img wbq975

Php类详解

发表于2004/11/4 11:25:00  1061人阅读

第一节--面向对象编程

面向对象编程被设计来为大型软件项目提供解决方案,尤其是多人合作的项目. 当源代码增长到一万行甚至更多的时候,每一个更动都可能导致不希望的副作用. 这种情况发生于模块间结成秘密联盟的时候,就像第一次世界大战前的欧洲.

:喻指模块间的关联度过高,相互依赖性太强.更动一个模块导致其它模块也必须跟着更动.

想像一下,如果有一个用来处理登录的模块允许一个信用卡处理模块来分享它的数据库连接. 当然出发点是好的,节省了进行另一个数据库连接的支出.然而有时,登录处理模块改变了其中一个变量的名字,就可能割断了两者间的协议.导致信用卡模块的处理出错,进而导致处理发票的模块出错. 很快地,体系中所有无关的模块都可能由此出错.

因此,我觉得有点戏剧性地,绝大多数程序员都对耦合和封装心存感激. 耦合是两个模块间依赖程度的量度. 耦合越少越好.我们希望能够从已有的项目中抽走一个模块并在另一个新项目中使用.

我们也希望在某个模块内部大规模的更动而不用担心对其他模块的影响. 封装的原则可以提供这个解决方案.模块被看待成相对独立,并且模块间的数据通信通过接口来进行. 模块不通过彼此的变量名来窥探另一个模块,它们通过函数来礼貌地发送请求.

封装是你可以在任何编程语言中使用的一个原则. PHP和许多面向过程的语言中,可以偷懒是很有诱惑的.没有什么可以阻止你通过模块来构建一个假想的WEB. 面向对象编程是使程序员不会违背封装原则的一种方法.

在面向对象编程中,模块被组织成一个个对象. 这些对象拥有方法和属性. 从抽象的角度来看,方法是一个对象的所做的动作,而属性是对象的特性.从编程角度来看,方法就是函数而属性是变量. 在一个理想化的面向对象体系中,每个部份都是一个对象. 体系由对象及对象间通过方法来形成的联系构成.

一个类定义了对象的属性. 如果你在烘烤一组甜饼对象,那么类将会是甜饼机. 类的属性和方法是被调用的成员. 人们可以通过说出数据成员或者方法成员来表达.

每种语言提供了不同的途径来访问对象. PHPC++中借用概念,提供一个数据类型用来在一个标识符下包含函数和变量。最初设计PHP的时候,甚至PHP3被开发出时,PHP并不打算提供开发超过10万行代码的大型项目的能力。随着PHPZend引擎的发展,开发大型项目变得有可能,但无论你的项目规模多大,用类来书写你的脚本将可以让代码实现重用。这是一个好主意,特别当你愿意与别人分享你的代码的时候。

有关对象的想法是计算机科学上最令人兴奋的概念之一。开始很难掌握它,但我可以保证,一旦你掌握了它,用它的思维来思考将会非常自然。

 

第二节--PHP5 的对象模型

PHP5有一个单重继承的,限制访问的,可以重载的对象模型. 本章稍后会详细讨论的继承”,包含类间的父-子关系. 另外,PHP支持对属性和方法的限制性访问. 你可以声明成员为private,不允许外部类访问. 最后,PHP允许一个子类从它的父类中重载成员.

:PHP4中没有private,只有public.private对于更好地实现封装很有好处.

PHP5的对象模型把对象看成与任何其它数据类型不同,通过引用来传递. PHP不要求你通过引用(reference)显性传递和返回对象. 在本章的最后将会详细阐述基于句柄的对象模型. 它是PHP5中最重要的新特性.

有了更直接的对象模型,基于句柄的体系有附加的优势: 效率提高, 占用内存少,并且具有更大的灵活性.

PHP的前几个版本中,脚本默认复制对象.现在PHP5只移动句柄,需要更少的时间. 脚本执行效率的提升是由于避免了不必要的复制. 在对象体系带来复杂性的同时,也带来了执行效率上的收益. 同时,减少复制意味着占用更少的内存,可以留出更多内存给其它操作,这也使效率提高.

:基于句柄,就是说两个对象可以指向同一块内存,既减少了复制动作,又减少对内存的占用.

Zand引擎2具有更大的灵活性. 一个令人高兴的发展是允许析构--在对象销毁之前执行一个类方法. 这对于利用内存也很有好处,PHP清楚地知道什么时候没有对象的引用,把空出的内存分配到其它用途.

当你声明一个类,你需要列出对象应有的所有变量和所有函数被称为属性和方法. 3.1.1中显示了一个类的构成. 注意在大括号({})内你只能声明变量或者函数. 3.1.2中显示了如何在一个类中定义三个属性和两个方法.

3.1.1

class Name extends Another Class

{

Access Variable Declaration

Access Function Declaration

}

3.1.2

<?php

//定义一个跟踪用户的类

class User

{

//属性

public $name;

private $password, $lastLogin;

//方法

public function __construct($name, $password)

{

$this->name = $name;

$this->password = $password;

$this->lastLogin = time();

$this->accesses++;

}

// 获取最后访问的时间

function getLastLogin()

{

return(date("M d Y", $this->lastLogin));

}

}

//创建一个对象的实例

$user = new User("Leon", "sdf123");

//获取最后访问的时间

print($user->getLastLogin() ."<br>n");

//打印用户名

print("$user->name<br>n");

?>

当你声明属性,你不需要指明数据类型. 变量可能是整型,字符串或者是另一个对象,这取决于实际情况.在声明属性时增加注释是一个好主意,标记上属性的含义和数据类型.

当你声明一个方法,你所做的和在类外部定义一个函数是一样的. 方法和属性都有各自的命名空间. 这意味着你可以安全地建立一个与类外部函数同名的方法,两者不会冲突. 例如,一个类中可以定义一个名为date()的方法. 但是你不能将一个方法命名为PHP的关键字,for或者while.

类方法可能包含PHP中所谓的type hint. Type hint 是另一个传递参数给方法的类的名字. 如果你的脚本调用方法并传递一个不是类的实例的变量,PHP将产生一个致命(fatal)错误” . 你可能没有给其它类型给出type hint,就像整型,字符串,或者布尔值. 在书写的时候, type hint是否应当包含数组类型仍存在争议.

Type hint是测试函数参数或者运算符的实例的数据类型的捷径. 你可能总是返回这个方法. 确认你强制让一个参数必须是哪种数据类型,如整型. 3.2.1 确保编译类只产生Widget的实例.

3.2.1

<?php

//组件

class Widget

{

public $name='none';

public $created=FALSE;

}

//装配器

class Assembler

{

public function make(Widget $w)

{

print("Making $w->name<br>n");

$w->created=TRUE;

}

}

//建立一个组件对象

$thing = new Widget;

$thing->name = 'Gadget';

//装配组件

Assembler::make($thing);

?>

除了传递参数的变量外,方法含有一个特殊的变量. 它代表类的个别实例. 你应当用这个来指向对象的属性和其它方法.一些面向对象的语言假设一个不合格的变量提交给本地属性,但在PHP中方法的任何变量只是在方法的一定范围内. 注意在User类的构造函数中这个变量的使用(3.1.2).

PHP在属性和方法声明前定义一个访问限定语,public,privateprotected. 另外,你可以用”static”来标记一个成员. 你也可以在类中声明常量. 本章稍后会有不同访问方式的相关讨论.

你可以在一行中列出相同访问方式的几个属性,用逗号来分隔它们. 3.1.2,User类有两个private属性--$password$lastLogin.

 

第四节--构造函数和析构函数

如果你在一个类中声明一个函数,命名为__construct,这个函数将被当成是一个构造函数并在建立一个对象实例时被执行. 清楚地说,__是两个下划线. 就像其它任何函数一样,构造函数可能有参数或者默认值. 你可以定义一个类来建立一个对象并将其属性全放在一个语句(statement).

你也可以定义一个名为__destruct的函数,PHP将在对象被销毁前调用这个函数. 它称为析构函数.

继承是类的一个强大功能. 一个类(子类/派生类)可以继承另一类(父类/基类)的功能. 派生类将包含有基类的所有属性和方法,并可以在派生类中加上其他属性和方法. 你也可以覆写基类的方法和属性. 就像3.1.2中显示的,你可以用extends关键字来继承一个类.

你可能想知道构造函数是如何被继承的. 当它们和其它方法一起被继承时,他们不会在创建对象时被执行.

如果你需要这个功能,你需要用第二章提到的::运算符. 它允许你指向一块命名空间. parent指向父类命名空间,你可以用parent::__construct来调用父类的构造函数.

一些面向对象语言在类之后命名构造函数. PHP的前几个版本也是如此,到现在这种方法仍然有效.也就是:如果你把一个类命名为Animal并且在其中建立一个命名也是Animal的方法,则这个方法就是构造函数.如果一个类的同时拥有__construt构造函数和与类名相同的函数,PHP将把__construct看作构造函数.这使得用以前的PHP版本所写的类仍然可以使用. 但新的脚本(PHP5)应当使用__construct.

PHP的这种新的声明构造函数的方法可以使构造函数有一个独一无二的名称,无论它所在的类的名称是什么. 这样你在改变类的名称时,就不需要改变构造函数的名称.

你可能在PHP中给构造函数一个像其它类方法一样的访问方式. 访问方式将会影响从一定范围内实例化对象的能力. 这允许实现一些固定的设计模式,Singleton模式.

析构函数,相反于构造函数. PHP调用它们来将一个对象从内存中销毁. 默认地,PHP仅仅释放对象属性所占用的内存并销毁对象相关的资源. 析构函数允许你在使用一个对象之后执行任意代码来清除内存.

PHP决定你的脚本不再与对象相关时,析构函数将被调用. 在一个函数的命名空间内,这会发生在函数return的时候. 对于全局变量,这发生于脚本结束的时候. 如果你想明确地销毁一个对象,你可以给指向该对象的变量分配任何其它值. 通常将变量赋值勤为NULL或者调用unset .

下面的例子中,计算从类中实例化的对象的个数. Counter类从构造函数开始增值,在析构函数减值.

一旦你定义了一个类,你可以用new来建立一个这个类的实例. 类的定义是设计图,实例则是放在装配线上的元件. New需要类的名称,并返回该类的一个实例. 如果构造函数需要参数,你应当在new后输入参数.

<?php

class Counter

{

private static $count = 0;

function __construct()

{

self::$count++;

}

function __destruct()

{

self::$count--;

}

function getCount()

{

return self::$count;

}

}

//建立第一个实例

$c = new Counter();

//输出1

print($c->getCount() . &quot;<br>n&quot;);

//建立第二个实例

$c2 = new Counter();

//输出2

print($c->getCount() . &quot;<br>n&quot;);

//销毁实例

$c2 = NULL;

//输出1

print($c->getCount() . &quot;<br>n&quot;);

?>

当你新建了一个实例,内存会被准备来存储所有属性. 每个实例有自己独有的一组属性. 但方法是由该类的所有实例共享的.

 

第五节--克隆

PHP5中的对象模型通过引用来调用对象, 但有时你可能想建立一个对象的副本,并希望原来的对象的改变不影响到副本 . 为了这样的目的,PHP定义了一个特殊的方法,称为__clone. __construct__destruct一样,前面有两个下划线.

默认地,__clone方法将建立一个与原对象拥有相同属性和方法的对象. 如果你想在克隆时改变默认的内容,你要在__clone中覆写(属性或方法).

克隆的方法可以没有参数,但它同时包含thisthat指针(that指向被复制的对象). 如果你选择克隆自己,你要小心复制任何你要你的对象包含的信息,thatthis. 如果你用__clone来复制. PHP不会执行任何隐性的复制,

下面显示了一个用系列序数来自动化对象的例子:

<?php

class ObjectTracker //对象跟踪器

{

private static $nextSerial = 0;

private $id;

private $name;

function __construct($name) //构造函数

{

$this->name = $name;

$this->id = ++self::$nextSerial;

}

function __clone() //克隆

{

$this->name = "Clone of $that->name";

$this->id = ++self::$nextSerial;

}

function getId() //获取id属性的值

{

return($this->id);

}

function getName() //获取name属性的值

{

return($this->name);

}

}

$ot = new ObjectTracker("Zeev's Object");

$ot2 = $ot->__clone();

//输出: 1 Zeev's Object

print($ot->getId() . " " . $ot->getName() . "<br>");

//输出: 2 Clone of Zeev's Object

print($ot2->getId() . " " . $ot2->getName() . "<br>");

?>

 

第六节--访问属性和方法

一个对象实例的属性是变量,就像PHP的其他变量一样. 但是你必须使用->运算符来引用它们. 不需要在属性前使用美元符$. 例如, 6.1中打印User对象的name属性那一行.

可以联用->,如果一个对象的属性包含了一个对象,你可以使用两个->运算符来得到内部对象的属性. 你甚至可以用双重引用的字符串来放置这些表达式. 6.5中的例子,对象House中的属性room包含了一组Room对象.

访问方法和访问属性类似. ->运算符用来指向实例的方法. 在例子6.1中调用getLastLogin就是. 方法执行起来和类外的函数几乎相同.

如果一个类从另一类中继承而来,父类中的属性和方法将在子类中都有效,即使在子类中没有声明. 像以前提到过的,继承是非常强大的. 如果你想访问一个继承的属性,你只需要像访问基类自己的属性那样引用即可,使用::运算符.

<?php

class Room

{

public $name;

function __construct($name="unnamed")

{

$this->name = $name;

}

}

class House

{

//array of rooms

public $room;

}

//create empty house

$home = new house;

//add some rooms

$home->room[] = new Room("bedroom");

$home->room[] = new Room("kitchen");

$home->room[] = new Room("bathroom");

//show the first room of the house

print($home->room[0]->name);

?>

PHP有两个特殊的命名空间:parent命名空间指向父类,self命名空间指向当前的类. 例子6.6中显示了如何用parent命名空间来调用父类中的构造函数. 同时也用self来在构造函数中调用另一个类方法.

<?php

class Animal //动物

{

public $blood; //热血or冷血属性

public $name;

public function __construct($blood, $name=NULL)

{

$this->blood = $blood;

if($name)

{

$this->name = $name;

}

}

}

class Mammal extends Animal //哺乳动物

{

public $furColor; //皮毛颜色

public $legs;

function __construct($furColor, $legs, $name=NULL)

{

parent::__construct("warm", $name);

$this->furColor = $furColor;

$this->legs = $legs;

}

}

class Dog extends Mammal

{

function __construct($furColor, $name)

{

parent::__construct($furColor, 4, $name);

self::bark();

}

function bark()

{

print("$this->name says 'woof!'");

}

}

$d = new Dog("Black and Tan", "Angus");

?>

第四章中介绍了如何调用函数. 对于对象的成员来是这样调用的:如果你需要在运行时确定变量的名称,你可以用$this->$Property这样的表达式. 如果你想调用方法,可以用$obj->$method().

你也可以用->运算符来返回一个函数的值,这在PHP以前的版本中是不允许的. 例如,你可以写一个像这样的表达式: $obj->getObject()->callMethod(). 这样避免了使用一个中间变量,也有助于实现某些设计模式,Factory模式.

 

第七节--类的静态成员

类的静态成员与一般的类成员不同: 静态成员与对象的实例无关,只与类本身有关. 他们用来实现类要封装的功能和数据,但不包括特定对象的功能和数据. 静态成员包括静态方法和静态属性.

静态属性包含在类中要封装的数据,可以由所有类的实例共享. 实际上,除了属于一个固定的类并限制访问方式外,类的静态属性非常类似于函数的全局变量

我们在下例中使用了一个静态属性Counter::$count. 它属于Counter,而不属于任何Counter的实例.你不能用this来引用它,但可以用self或其它有效的命名表达. 在例子中,getCount方法返回self::$count,而不是Counter::$count.

静态方法则实现类需要封装的功能,与特定的对象无关. 静态方法非常类似于全局函数. 静态方法可以完全访问类的属性,也可以由对象的实例来访问,不论访问的限定语是否是什么.

6.3例中,getCount是一个普通的方法,->来调用. PHP建立一个this变量,尽管方法没有使用到.但是,getCount不属于任何对象.在有些情况下,我们甚至希望在不存在有效的对象时调用它,那么就应该使用静态方法. PHP将不在静态方法内部建立this变量,即使你从一个对象中调用它们.

例子6.76.3改变getCount为静态方法而来. Static关键字不能阻止一个实例用->运算符来调用getCount,PHP将不在方法内部建立this变量.如果你使用this->来调用,将会出错.

//6.3例指第四节--构造函数和析构函数中的例子(参看前文),通过两个例子的比较,你可以很好掌握//static方法与普通方法之间的区别.

你可以写一个方法通过判断this是否建立来显示是否它被静态地或者非静态地调用. 当然,如果你用了static 关键字,不管它怎样被调用,这个方法总是静态的.你的类也可以定义常量属性,不需要使用public static,只需要用const关键字即可. 常量属性总是静态的.它们是类的属性,而不是实例化该类的对象的属性.

Listing 6.7 Static members

<?php

class Counter

{

private static $count = 0;

const VERSION = 2.0;

function __construct()

{

self::$count++;

}

function __destruct()

{

self::$count--;

}

static function getCount()

{

return self::$count;

}

};

//创建一个实例,__construct()将执行

$c = new Counter();

//输出 1

print(Counter::getCount() . "<br>n");

//输出类的版本属性

print("Version used: " . Counter::VERSION . "<br>n");

?>

 

第八节--访问方式

PHP5的访问方式允许限制对类成员的访问. 这是在PHP5中新增的功能,但在许多面向对象语言中都早已存在. 有了访问方式,才能开发一个可靠的面向对象应用程序,并且构建可重用的面向对象类库.

C++Java一样,PHP有三种访问方式:public,privateprotected. 对于一个类成员的访问方式,可以是其中之一. 如果你没有指明访问方式,默认地访问方式为public. 你也可以为静态成员指明一种访问方式,将访问方式放在static关键字之前(public static).

Public成员可以被毫无限制地访问.类外部的任何代码都可以读写public属性. 你可以从脚本的任何地方调用一个public方法. PHP的前几个版本中,所有方法和属性都是public, 这让人觉得对象就像是结构精巧的数组.

Private(私有)成员只在类的内部可见. 你不能在一个private属性所在的类方法之外改变或读取它的值. 同样地,只有在同一个类中的方法可以调用一个private方法. 继承的子类也不能访问父类中的private 成员.

要注意,类中的任何成员和类的实例都可以访问private成员. 看例子6.8,equals方法将两个widget进行比较.==运算符比较同一个类的两个对象,但这个例子中每个对象实例都有唯一的ID.equals方法只比较nameprice. 注意equals方法如何访问另一个Widget实例的private属性. JavaC都允许这样的操作.

Listing 6.8 Private members

<?php

class Widget

{

private $name;

private $price;

private $id;

public function __construct($name, $price)

{

$this->name = $name;

$this->price = floatval($price);

$this->id = uniqid();

}

//checks if two widgets are the same 检查两个widget是否相同

public function equals($widget)

{

return(($this->name == $widget->name)AND ($this->price == $widget->price));

}

}

$w1 = new Widget('Cog', 5.00);

$w2 = new Widget('Cog', 5.00);

$w3 = new Widget('Gear', 7.00);

//TRUE

if($w1->equals($w2))

{

print("w1 and w2 are the same<br>n");

}

//FALSE

if($w1->equals($w3))

{

print("w1 and w3 are the same<br>n");

}

//FALSE, == includes id in comparison

if($w1 == $w2) //不等,因为ID不同

{

print("w1 and w2 are the same<br>n");

}

?>

如果你对面向对象编程不熟悉,你可能想知道用private成员的目的是什么. 你可以回忆一下封装和耦合的想法,这在本章开头我们有讨论过. Private成员有助于封装数据. 他们可以隐藏在一个类内部而不被类外部的代码接触到. 同时他们还有助于实现松散的耦合. 如果数据结构外的代码不能直接访问内部属性,那么就不会产生一个隐性的关联性.

当然,大部分private属性仍然可以被外部代码共享. 解决方法是用一对public方法,一个是get(获取属性的值),另一个是set(设置属性的值). 构造函数也接受属性的初始值. 这使得成员间的交流通过一个狭窄的,经过良好限定的接口来进行. 这也提供改变传递给方法的值的机会. 注意在例子6.8,构造函数如何强制使price成为一个float(floadval()).

Protected(受保护的) 成员能被同个类中的所有方法和继承出的类的中所有方法访问到. Public属性有违封装的精神,因为它们允许子类依赖于一个特定的属性来书写.protected方法则不会带来这方面的担忧.一个使用protected方法的子类需要很清楚它的父类的结构才行.

例子6.9由例子6.8改进而得到,包含了一个Widget的子类Thing. 注意Widget现在有一个叫作getNameprotected方法. 如果Widget的实例试图调用protected方法将会出错: $w1->getName()产生了一个错误. 但子类Thing中的getName方法可以调用这个protected方法.当然对于证明Widget::getName方法是protected,这个例子显得过于简单. 在实际情况下,使用protected方法要依赖于对对象的内部结构的理解.

Listing 6.9 Protected members

<?php

class Widget

{

private $name;

private $price;

private $id;

public function __construct($name, $price)

{

$this->name = $name;

$this->price = floatval($price);

$this->id = uniqid();

}

//checks if two widgets are the same

public function equals($widget)

{

return(($this->name == $widget->name)AND ($this->price == $widget->price));

}

protected function getName()

{

return($this->name);

}

}

class Thing extends Widget

{

private $color;

public function setColor($color)

{

$this->color = $color;

}

public function getColor()

{

return($this->color);

}

public function getName()

{

return(parent::getName());

}

}

$w1 = new Widget('Cog', 5.00);

$w2 = new Thing('Cog', 5.00);

$w2->setColor('Yellow');

//TRUE (still!) 结果仍然为真

if($w1->equals($w2))

{

print("w1 and w2 are the same<br>n");

}

//print Cog 输出 Cog

print($w2->getName());

?>

一个子类可能改变通过覆写父类方法来改变方法的访问方式,尽管如此,仍然有一些限制. 如果你覆写了一个public类成员,他子类中必须保持public. 如果你覆写了一个protected成员,它可保持protected或变成public.Private成员仍然只在当前类中可见. 声明一个与父类的private成员同名的成员将简单地在当前类中建立一个与原来不同的成员. 因此,在技术上你不能覆写一个private成员.

Final关键字是限制访问成员方法的另一个方法. 子类不能覆写父类中标识为final的方法. Final关键字不能用于属性.

:PHP5的面向对象模型仍然不够完善,final不像Java中那样对Data,Method甚至Class都可以用.

 

第九节--绑定

除了限制访问,访问方式也决定哪个方法将被子类调用或哪个属性将被子类访问. 函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定.

在计算机语言中有两种主要的绑定方式静态绑定和动态绑定. 静态绑定发生于数据结构和数据结构间,程序执行之前. 静态绑定发生于编译期, 因此不能利用任何运行期的信息. 它针对函数调用与函数的主体,或变量与内存中的区块. 因为PHP是一种动态语言,它不使用静态绑定. 但是可以模拟静态绑定.

动态绑定则针对运行期产生的访问请求,只用到运行期的可用信息. 在面向对象的代码中,动态绑定意味着决定哪个方法被调用或哪个属性被访问,将基于这个类本身而不基于访问范围.

Publicprotected成员的动作类似于PHP的前几个版本中函数的动作,使用动态绑定. 这意味着如果一个方法访问一个在子类中被覆写的类成员,并是一个子类的实例,子类的成员将被访问(而不是访问父类中的成员).

看例子6.10. 这段代码输出” Hey! I am Son.” 因为当PHP调用getSalutation, 是一个Son的实例,是将Father中的salutation覆写而来. 如果salutationpublic,PHP将产生相同的结果. 覆写方法的操作很类似.Son,对于identify的调用绑定到那个方法.

即使在子类中访问方式被从protected削弱成public, 动态绑定仍然会发生. 按照访问方式使用的原则,增强对于类成员的访问限制是不可能的. 所以把访问方式从public改变成protected不可能进行.

Listing 6.10 Dynamic binding 动态绑定

<?php

class Father

{

protected $salutation = "Hello there!"; //问候

public function getSalutation()

{

print("$this->salutationn");

$this->identify();

}

protected function identify()

{

print("I am Father.<br>n");

}

}

class Son extends Father

{

protected $salutation = "Hey!"; //父类中的protected $salutation 被覆写

protected function identify() //父类中的protected identify() 被覆写

{

print("I am Son.<br>n");

}

}

$obj = new Son();

$obj->getSalutation(); //输出Hey! I am Son.

?>

//: 在子类中没有覆写getSalutation(),但实际上仍然存在一个getSalutation().这个类中的$salutationidentify()

//Son子类的实例中的getSalutation()方法动态绑定,所以调用Son的实例的getSalutation()方法,

//将调用Son类中的成员salutationidentify(),而不是父类中的成员salutationidentify().

Private成员只存在于它们所在的类内部. 不像publicprotected成员那样,PHP模拟静态绑定. 看例子6.11. 它输出”Hello there! I am Father.”,尽管子类覆写了salutation的值. 脚本将this->salutation和当前类Father绑定. 类似的原则应用于private方法identify().

Listing 6.11 Binding and private members

<?php

class Father

{

private $salutation = "Hello there!";

public function getSalutation()

{

print("$this->salutationn");

$this->identify();

}

private function identify()

{

print("I am Father.<br>n");

}

}

class Son extends Father

{

private $salutation = "Hey!";

private function identify()

{

print("I am Son.<br>n");

}

}

$obj = new Son();

$obj->getSalutation(); //输出Hello there! I am Father.

?>

动态绑定的好处是允许继承类来改变父类的行为,同时可以保持父类的接口和功能. 看例子6.12. 由于使用了动态绑定,deleteUser中被调用的isAuthorizedversion 可以由对象的类型来确定. 如果是一个普通的user,PHP调用User::isAuthorized会返回FALSE.如果是一个AuthorizedUser的实例,PHP调用AuthorizedUser::isAuthorized,将允许deleteUser顺利执行.

:用一句话说清楚,就是对象类型与方法,属性绑定. 调用一个父类与子类中都存在的方法或访问一个属性时,会先判断实例属于哪种对象类型,再调用相应的类中的方法和属性.

Listing 6.12 动态绑定的好处

<?php

class User //用户

{

protected function isAuthorized() //是否是验证用户

{

return(FALSE);

}

public function getName() //获得名字

{

return($this->name);

}

public function deleteUser($username) //删除用户

{

if(!$this->isAuthorized())

{

print("You are not authorized.<br>n");

return(FALSE);

}

//delete the user

print("User deleted.<br>n");

}

}

class AuthorizedUser extends User //认证用户

{

protected function isAuthorized() //覆写isAuthorized()

{

return(TRUE);

}

}

$user = new User;

$admin = new AuthorizedUser;

//not authorized

$user->deleteUser("Zeev");

//authorized

$admin->deleteUser("Zeev");

?>

为什么private的类成员模拟静态绑定? 为了回答这个问题, 你需要回忆一下为什么需要有private成员.什么时候用它们来代替protected成员是有意义的?

private成员只有当你不想让子类继承改变或特殊化父类的行为时才用到. 这种情况比你想像的要少. 通常来说,一个好的对象分层结构应当允许绝大多数功能被子类特殊化,改进,或改变这是面向对象编程的基础之一. 一定的情况下需要private方法或变量,例如当你确信你不想允许子类改变父类中的某个特定的部份.

 

第十节--抽象方法和抽象类

面向对象程序通过类的分层结构构建起来. 在单重继承语言如PHP, 类的继承是树状的. 一个根类有一个或更多的子类,再从每个子类继承出一个或更多下一级子类. 当然,可能存在多个根类,用来实现不同的功能. 在一个良好设计的体系中,每个根类都应该有一个有用的接口, 可以被应用代码所使用. 如果我们的应用代码被设计成与根类一起工作,那么它也可以和任何一个从根类继承出来的子类合作.

抽象方法是就像子类中一般的方法的占位符(占个地方但不起作用),它与一般方法不同没有任何代码. 如果类中存在一个或更多抽象方法, 那么这个类就成了抽象类. 你不能实例化抽象类. 你必须继承它们,然后实例化子类. 你也可以把抽象类看成是子类的一个模板.

如果你覆写所有的抽象方法, 子类就变成一个普通的类. 如果没有覆写所有方法, 子类仍是抽象的. 如果一个类中中包含有抽象方法(哪怕只有一个), 你必须声明这个类是抽象的, class关键字前加上abstract.

声明抽象方法的语法与声明一般方法不同. 抽象方法的没有像一般方法那样包含在大括号{}中的主体部份,并用分号;来结束.

在例子6.13, 我们定义了一个含有getArea方法的类Shape. 但由于不知道形状不可能确定图形的面积,确良我们声明了getArea方法为抽象方法. 你不能实例化一个Shape对象,但你可以继承它或在一个表达式中使用它, 就像例6.13中那样.

如果你建立了一个只有抽象方法的类,你就定义了一个接口(interface). 为了说明这种情况, PHP中有interface implements关键字. 你可以用interface来代替抽象类, implements来代替extends来说明你的类定义或使用一个接口. 例如, 你可以写一个myClass implements myIterface. 这两种方法可以依个人偏爱来选择.

/*:

两种方法即指:

1. abstract class aaa{} (注意aaa中只有抽象方法,没有一般方法)

class bbb extends aaa{} (bbb中覆写aaa中的抽象方法)

2. interface aaa{}

class bbb implements aaa{} (bbb中覆写aaa中的抽象方法)

*/

Listing 6.13 Abstract classes

<?php

//abstract root class 抽象根类

abstract class Shape

{

abstract function getArea(); //定义一个抽象方法

}

//abstract child class 抽象子类

abstract class Polygon extends Shape //多边形

{

abstract function getNumberOfSides();

}

//concrete class 实体类 三角形类

class Triangle extends Polygon

{

public $base;

public $height;

public function getArea() //覆写计算面积方法

{

return(($this->base * $this->height)/2);

}

public function getNumberOfSides() //覆写边数统计方法

{

return(3);

}

}

//concrete class 实体类四边形

class Rectangle extends Polygon

{

public $width;

public $height;

public function getArea()

{

return($this->width * $this->height);

}

public function getNumberOfSides()

{

return(4);

}

}

//concrete class 实体类 圆形

class Circle extends Shape

{

public $radius;

public function getArea()

{

return(pi() * $this->radius * $this->radius);

}

}

//concrete root class 定义一个颜色类

class Color

{

public $name;

}

$myCollection = array(); //建立形状的集合,放入数组

//make a rectangle

$r = new Rectangle;

$r->width = 5;

$r->height = 7;

$myCollection[] = $r;

unset($r);

//make a triangle

$t = new Triangle;

$t->base = 4;

$t->height = 5;

$myCollection[] = $t;

unset($t);

//make a circle

$c = new Circle;

$c->radius = 3;

$myCollection[] = $c;

unset($c);

//make a color

$c = new Color;

$c->name = "blue";

$myCollection[] = $c;

unset($c);

foreach($myCollection as $s)

{

if($s instanceof Shape) //如果$sShape类的实例

{

print("Area: " . $s->getArea() .

"<br>n");

}

if($s instanceof Polygon)

{

print("Sides: " . $s->getNumberOfSides() . "<br>n");

}

if($s instanceof Color)

{

print("Color: $s->name<br>n");

}

print("<br>n");

}

?>

 

第十一节--重载

PHP4中已经有了重载的语法来建立对于外部对象模型的映射,就像JavaCOM那样. PHP5带来了强大的面向对象重载,允许程序员建立自定义的行为来访问属性和调用方法.

重载可以通过__get, __set, and __call几个特殊方法来进行. Zend引擎试图访问一个成员并没有找到时,PHP将会调用这些方法.

在例6.14,__get__set代替所有对属性变量数组的访问. 如果必要,你可以实现任何类型你想要的过滤. 例如,脚本可以禁止设置属性值, 在开始时用一定的前缀或包含一定类型的值.

__call方法说明了你如何调用未经定义的方法. 你调用未定义方法时,方法名和方法接收的参数将会传给__call方法, PHP传递__call的值返回给未定义的方法.

Listing 6.14 User-level overloading

<?php

class Overloader

{

private $properties = array();

function __get($property_name)

{

if(isset($this->properties[$property_name]))

{

return($this->properties[$property_name]);

}

else

{

return(NULL);

}

}

function __set($property_name, $value)

{

$this->properties[$property_name] = $value;

}

function __call($function_name, $args)

{

print("Invoking $function_name()<br>n");

print("Arguments: ");

print_r($args);

return(TRUE);

}

}

$o = new Overloader();

//invoke __set() 给一个不存在的属性变量赋值,激活__set()

$o->dynaProp = "Dynamic Content";

//invoke __get() 激活__get()

print($o->dynaProp . "<br>n");

//invoke __call() 激活__call()

$o->dynaMethod("Leon", "Zeev");

?>

 

第十二节--类的自动加载

当你尝试使用一个未定义的类时,PHP会报告一个致命错误. 解决方法就是添加一个类,可以用include包含一个文件. 毕竟你知道要用到哪个类. 但是,PHP提供了类的自动加载功能, 这可以节省编程的时间. 当你尝试使用一个PHP没有组织到的类, 它会寻找一个__autoload的全局函数. 如果存在这个函数,PHP会用一个参数来调用它,参数即类的名称.

例子6.15说明了__autoload是如何使用的. 它假设当前目录下每个文件对应一个类. 当脚本尝试来产生一个类User的实例,PHP会执行__autoload. 脚本假设class_User.php中定义有User.. 不管调用时是大写还是小写,PHP将返回名称的小写.

Listing 6.15 Class autoloading

<?php

//define autoload function

function __autoload($class)

{

include("class_" . ucfirst($class) . ".php");

}

//use a class that must be autoloaded

$u = new User;

$u->name = "Leon";

$u->printName();

?>

 

第十三节--对象串行化

串行化可以把变量包括对象,转化成连续bytes数据. 你可以将串行化后的变量存在一个文件里或在网络上传输. 然后再反串行化还原为原来的数据. 你在反串行化类的对象之前定义的类,PHP可以成功地存储其对象的属性和方法. 有时你可能需要一个对象在反串行化后立即执行. 为了这样的目的,PHP会自动寻找__sleep__wakeup方法.

当一个对象被串行化,PHP会调用__sleep方法(如果存在的话). 在反串行化一个对象后,PHP 会调用__wakeup方法. 这两个方法都不接受参数. __sleep方法必须返回一个数组,包含需要串行化的属性. PHP会抛弃其它属性的值. 如果没有__sleep方法,PHP将保存所有属性.

例子6.16显示了如何用__sleep__wakeup方法来串行化一个对象. Id属性是一个不打算保留在对象中的临时属性. __sleep方法保证在串行化的对象中不包含id属性. 当反串行化一个User对象,__wakeup方法建立id属性的新值. 这个例子被设计成自我保持. 在实际开发中,你可能发现包含资源(如图像或数据流)的对象需要这些方法.

Listing 6.16 Object serialization

<?php

class User

{

public $name;

public $id;

function __construct()

{

//give user a unique ID 赋予一个不同的ID

$this->id = uniqid();

}

function __sleep()

{

//do not serialize this->id 不串行化id

return(array("name"));

}

function __wakeup()

{

//give user a unique ID

$this->id = uniqid();

}

}

//create object 建立一个对象

$u = new User;

$u->name = "Leon";

//serialize it 串行化 注意不串行化id属性,id的值被抛弃

$s = serialize($u);

//unserialize it 反串行化 id被重新赋值

$u2 = unserialize($s);

//$u and $u2 have different IDs $u$u2有不同的ID

print_r($u);

print_r($u2);

?>

 

第十四节--命名空间

命名变量,函数和类是挺困难的,除了要考虑到变量的名称要易于理解,还要担心是否这个名称在其它某个地方已经使用过. 在一小段脚本中,第二个问题是基本问题. 当你考虑重用你的代码, 在这之后的项目代码必须避免使用你用过的命名. 通常来说,可重用的代码总是包含在函数或类中, 需要处理许多可能发生的命名冲突. 但函数和类之间也可能发生命名冲突. 你可以尝试避免出现这种情况,通过在所有类前添加前缀的方法,或者你可以使用namespace语句.

Namespace关键字给一块代码命名. 在这个代码块外部,脚本必须用操作符::加上命名空间的名称来引用这个代码块. 引用静态的类成员也是用相同的方法. 在命名空间内代码不需要声明命名空间, 它本身就是默认的. 这种方法比添加前缀的方法好. 你的代码可由此变得更加紧凑和可读.

你可能想知道是否可以建立分层的(嵌套的)命名空间. 答案是不可以. 但你可以在命名空间名称后加上冒号, 你可以再次调用在名称中不包含冒号的变量,函数和类. 命名空间允许存在冒号,只要不是第一个字符和最后一个字符或接着另一个冒号. 命名空间的名称中的冒号对于PHP来说没有任何意义, 但如果你用他们来区分逻辑上的区块, 他们可以很好地说明你的代码中的父子(parent-child)关系.

/* : 即可以使用这样:

namespace animal:dog {}

namespace animal:pig {}

用冒号来说明parent-child关系.

*/

你可能在一个命名空间语句内没有包含函数,类或常量定义以外的任何东西. 这将阻止你使用他们来改进旧的使用全局变量的函数库. 命名空间最适合于面向对象. 命名空间内的常量与类中的常量使用相同的语法.

例子6.17显示了如何使用命名空间.

Listing 6.17 Using a namespace

<?php

namespace core_php:utility

{

class textEngine

{

public function uppercase($text) //大写

{

return(strtoupper($text));

}

}

//make non-OO interface 建立一个非OO的接口

function uppercase($text)

{

$e = new textEngine;

return($e->uppercase($text));

}

}

//test class in namespace 测试命名空间中的类

$e = new core_php:utility::textEngine;

print($e->uppercase("from object") . "<br>");

//test function in namespace 测试命名空间中的函数

print(core_php:utility::uppercase("from function") . "<br>");

//bring class into global namespace 把类导入全局命名空间

import class textEngine from core_php:utility;

$e2 = new textEngine;

?>

Import语句把命名空间中的某个部份导入全局的命名空间.

要导入单一的命名空间的成员,可以指定类型为constant,functionclass,接着写上成员的名称;

//import class XXX

如果你想导入某一特定类型的所有成员,你可以用*来代替名称;

// import constant * 导入所有常量

如果你想导入所有类型的所有成员,*即可.

// import *

在成员之后,from关键字加上命名空间的名称.

// import class textEngine from core_php:utility;

总之你要写成像import * from myNamespace import class textEngine from core_php:utility这样的语句,就像例6.17中那样.

 

第十五节--Zend引擎的发展

本章的最后一节,Zeev讨论了Zend引擎带来的对象模型,特别提到它与PHP的前几个版本中的模型有什么不同.

1997年夏天,我们开发出PHP3, 我们没有计划要使PHP具备面向对象的能力. 当时没有任何与类和对象有关的想法. PHP3是一个纯粹面向过程的语言. 但是,1997.8.27的晚上PHP3 alpha版中增加了对类的支持. 增加一个新特性给PHP,当时仅需要极少的讨论,因为当时探索PHP的人太少. 于是从1997年八月起, PHP迈出了走向面向对象编程语言的第一步.

确实,这只是第一步. 因为在这个设计中只有极少的相关的想法,对于对象的支持不够强大. 这个版本中使用对象仅是访问数组的一个很酷的方法而已. 取代使用$foo[“bar”],你可以使用看起来更漂亮的$foo->bar. 面向对象方法的主要的优势是通过成员函数或方法来储存功能. 例子6.18中显示了一个典型的代码块. 但是它和例6.19中的做法其实并没有太大不同.

Listing 6.18 PHP 3 object-oriented programming PHP3中的面向对象编程

<?php

class Example

{

var $value = "some value";

function PrintValue()

{

print $this->value;

}

}

$obj = new Example();

$obj->PrintValue();

?>

Listing 6.19 PHP 3 structural programming PHP3 PHP3中的结构化编程

<?php

function PrintValue($arr)

{

print $arr["value"];

}

function CreateExample()

{

$arr["value"] = "some value";

$arr["PrintValue"] = "PrintValue";

return $arr;

}

$arr = CreateExample();

//Use PHP's indirect reference

$arr["PrintValue"]($arr);

?>

以上我们在类中写上两行代码,或者显示地传递数组给函数. 但考虑到PHP3中这两种选择并没有任何不同,我们仍然可以仅把对象模型当成一种语法上的粉饰来访问数组.

想要用PHP来进行面向对象开发的人们,特别是想使用设计模式的人,很快就发现他们碰壁了. 幸运地,当时(PHP3时代)没有太多人想用PHP来进行面向对象开发.

PHP4改变了这种情况. 新的版本带来了引用(reference)的概念, 它允许PHP的不同标识符指向内存中的同一个地址. 这意味着你可以使用两个或更多的名称来给同一个变量命名,就像例6.20那样.

Listing 6.20 PHP 4 references PHP4中的引用

<?php

$a = 5;

//$b points to the same place in memory as $a $b$a指向内存中同个地址

$b = &$a;

//we're changing $b, since $a is pointing to 改变$b,指向的地址改变

//the same place - it changes too $a指向的地址也改变

$b = 7;

//prints 7 输出7

print $a;

?>

由于构建一个指向彼此的对象网络是所有面向对象设计模式的基础,这个改进具有非常重大的意义.当引用允许建立更多强大的面向对象应用程序, PHP对待对象和其它类型数据相同的做法带给开发者极大的痛苦.就像任何PHP4的程序员将会告诉你的, 应用程序将会遭遇WTMA(Way Too Many Ampersands过多&)综合症. 如果你想构建一个实际应用,你会感到极为痛苦,看看例6.21你就明白.

Listing 6.21 Problems with objects in PHP 4 PHP4中使用对象的问题

class MyFoo

{

function MyFoo()

{

$this->me = &$this;

$this->value = 5;

}

function setValue($val)

{

$this->value = $val;

}

function getValue()

{

return $this->value;

}

function getValueFromMe()

{

 return $this->me->value;

}

}

function CreateObject($class_type)

{

switch ($class_type)

{

case "foo":

$obj = new MyFoo();

break;

case "bar":

$obj = new MyBar();

break;

}

return $obj;

}

$global_obj = CreateObject ("foo");

$global_obj->setValue(7);

print "Value is " . $global_obj->getValue() . "n";

print "Value is " . $global_obj->getValueFromMe() . "n";

让我们一步步来讨论. 首先,有一个MyFoo.在构造函数里,我们给$this->me一个引用,并设定
我们有其它三个成员函数: 一个设定this->value的值;一个返回this->value的值;另一个返回this->value->me的值. 但是--$this不是相同的东西吗? MyFoo::getValue()MyFoo::getValueFromMe()返回的值不是一样的吗?

首先,我们调用CreateObject("foo"),这会返回一个MyFoo类型的对象. 然后我们调用MyFoo::setValue(7). 最后,我们调用MyFoo::getValue() MyFoo::getValueFromMe(), 期望得到返回值7.

当然,如果我们在任何情况下都得到7, 以上这个例子将不是本书中最没有意义的例子. 所以我相信你已经猜到我们得不到两个7这样的结果.

但是我们将得到什么结果,并且更重要地,为什么呢?

我们将得到的结果分别是75. 至于为什么—--有三个很好的理由.

首先,看构造函数. 当在构造函数内部,我们在thisthis->me间建立引用. 换句话说,thisthis->me是同个东西. 但是我们是在构造函数内. 当构造函数结束,PHP要重新建立对象(new MyFoo的结果,28)分配给$obj. 因为对象没有特殊化对待,就像其它任何数据类型一样,赋值XY意味着YX的一个副本. 也就是说,obj将是new MyFoo的一个副本,new MyFoo是一个存在于构造函数的对象. Obj->me怎么样呢? 因为它是一个引用,它原封不动仍然指向原来的对象—this. Voila-objobj->me不再是同个东西了改变其中一个另一个不变.

以上是第一条理由. 还有其它类似于第一条的理由. 奇迹般地我们打算克服实例化对象这个问题(28). 一旦我们把CreateObject返回的值赋给global_object,我们仍然要撞上相同的问题—global_object将变成返回值的一个副本,并且再次地,global_objectglobal_object->me将不再相同. 这就是第二条理由.

但是,事实上我们还走不了那么远一旦CreateObject返回$obj,我们将破坏引用(34) . 这就是第三条理由.

那么,我们如何改正这些? 有两个选择. 一是在所有地方增加&符号,就像例6.22那样(24, 28, 31, 37). .如果你幸运地使用上了PHP5,你可以忘了以上这一切,PHP5会自动为你考虑这些. 如果你想知道PHP5是如何考虑这些问题的,继续阅读下去.

Listing 6.22 WTMA syndrome in PHP 4 PHP4中的WTMA综合症

class MyFoo

{

function MyFoo()

{

$this->me = &$this;

$this->value = 2;

}

function setValue($val)

{

$this->value = $val;

}

function getValue()

{

return $this->value;

}

function getValueFromMe()

{

return $this->me->value;

}

}

function &CreateObject($class_type)

{

switch ($class_type)

{

case "foo":

$obj =& new MyFoo();

break;

case "bar":

$obj =& new MyBar();

break;

}

return $obj;

}

$global_obj =& CreateObject ("foo");

$global_obj->setValue(7);

print "Value is " . $global_obj->getValue() . "n";

print "Value is " . $global_obj->getValueFromMe() . "n";

PHP5是第一个把对象看成与其它类型数据不同的PHP版本. 从用户的角度看,这证明它非常明白的方式PHP5,对象总是通过引用来传递,而其它类型数据(integer,string,array)都是通过值来传递. 最显著地,没有必要再用&符号来表示通过引用来传递对象了.

面向对象编程广泛利用了对象网络和对象间的复杂关系,这些都需要用到引用. PHP的前些版本中,需要显示地指明引用. 因此, 现在默认用引用来移动对象,并且只有在明确要求复制时才复制对象,这样比以前更好.

它是如何实现的呢?

PHP5之前,所有值都存在一个名为zval(Zend Value)的特殊结构里. 这些值可以存入简单的值,如数字和字符串,或复杂的值如数组和对象. 当值传给函数或从函数返回时,这些值会被复制,在内存的另一个地址建立一个带有相同内容的结构.

PHP5,值仍存为zval结构中,但对象除外. 对象存在一个叫做Object Store的结构里,并且每个对象有一个不同的ID. Zval,不储存对象本身,而是存着对象的指针. 当复制一个持有对象的zval结构,例如我们把一个对象当成参数传给某个函数,我们不再复制任何数据. 我们仅仅保持相同的对象指针并由另一个zval通知现在这个特定的对象指向的Object Store. 因为对象本身位于Object Store,我们对它所作的任何改变将影响到所有持有该对象指针的zval结构.这种附加的间接作用使PHP对象看起来就像总是通过引用来传递,用透明和有效率的方式.

使用PHP5,我们现在可以回到示例6.21,除去所有的&符号, 一切代码都仍然可以正常工作.当我们在构造函数(4)中持有一个引用时一个&符号都不用.

阅读全文
0 0

相关文章推荐

img
取 消
img