body{font-size:14px;line-height:150%}
Php 3.x与4.x中关于对象编程的不兼容问题
“面向对象”听起来是个很流行的词汇,似乎到了如果你还没有OOP,那不如回家种白菜的地步。
Php从版本3.x开始支持对象编程,虽然它的Class从一开始就饱受程序员们的指责,但它的确给我们带来了意外的惊喜。一路跌跌撞撞走来,到了4.x,Php已经相当OOP了。当然,它对于类变量的处理依然不能让人满意,没有私有、公有、保护、静态的声明方法。Php面向对象的可用性不在本文讨论范围内。
伴随着4.x中关于对象编程的完善,Php team给我们带来了些许“麻烦”:3.x和4.x中关于对象编程的一些游戏规则改变了,不兼容。笔者就实际开发过程遇到的问题稍作讨论,相信有些问题可能笔者尚未遇到,欢迎诸位补充、共赏。
三、魔法函数说
4.x中开始出现的魔法函数,用以达到被Php小组宣称的“不可思议”(magical)的效果。
魔法函数一词直译于“magic function”。
这是一类具有保留名字的类函数,Php小组在推荐中有这样的表述:用户定义自己的类函数不应该以两个下划线“__”开始,因为这可能与现在或后续版本中的魔法函数发生重名冲突并将严重影响用户定义类的正常工作。所以,如果你过去的类代码中如果定义了“__xxx”形式的方法,强烈建议你修改它们。
魔法函数会在类遇到某些事件时自动触发,好像某些数据库软件中的触发器。截至php-4.3.2RC4,共有两个魔法函数:__sleep和__wakeup。从字面上看就是说这两个函数分别在对象发生睡着动作或苏醒动作时触发,事实上是__sleep在对象被序列化(serialize)时触发,__wakeup在对象被反序列化(unserialize)时触发。
说到这里,可能已经让人觉得枯燥了,因为还没有出现什么意外的惊喜激励我们。看看下面的例子。
1、
class ABaby
{
var $fileName,$fileOpen,$offset,$length;
function ABaby(){
$this->fileName='ABaby.class.php';
$this->fileOpen=fopen($this->fileName,'rb');
$this->offset=0;
$this->length=0;
}
function getData($offset,$length){
$this->offset=$offset;
$this->length=$length;
fseek($this->fileOpen,$offset);
return(fread($this->fileOpen,$length));
}
function getLastData(){
return($this->getData($this->offset,$this->length));
}
}
$ABaby=new ABaby();
echo($ABaby->getData(7,77));
$ABabyBed=serialize($ABaby);
$newABaby=unserialize($ABabyBed);
echo($newABaby->getLastData());
注释:
这个类的任务是打开一个文件读取指定位置和字节长度数据,并可以返回最近一次读取的数据。实例化一个对象后,首先读取指定文件第7个字节开始共77字节的内容,然后把这个类序列化保存进一个字串,再从这个字串反序列化出对象后输出最近一次读取的数据。这次不用__wakeup。
将上面代码保存成文件ABaby.class.php并运行。出错了:supplied argument is not a valid stream resource!这是因为序列化时解析器把$fileOpen回归了它的本来面目——integer,已经不是原来意义上的文件流操作句柄(resource),实施上,这时候文件还是处于打开状态的,可以把最后一行换作“echo($ABaby->getLastData());”证实。同样的情况还会发生在数据库连接、网络连接等其他句柄上,读者可以自行测试一下。
下面我们通过魔法函数__wakeup修正。当然不是一定要用__wakeup,比如你也可以首先反序列化得到对象$newABaby中,然后将实例变量$offset和$length保存到另外一处,然后调用$newABaby的构造器,然后调用getData()方法,这样就可以达到调用getLastData()符合预期的目的。一连这么多个然后......现在看__wakeup的魔力。
2、
class ABaby
{
var $fileName,$fileOpen,$offset,$length;
function ABaby(){
$this->fileName='ABaby.class.php';
$this->fileOpen=fopen($this->fileName,'rb');
$this->offset=0;
$this->length=0;
}
function getData($offset,$length){
$this->offset=$offset;
$this->length=$length;
fseek($this->fileOpen,$offset);
return(fread($this->fileOpen,$length));
}
function getLastData(){
return($this->getData($this->offset,$this->length));
}
function __wakeup(){
$this->fileOpen=fopen($this->fileName,'rb');
}
}
$ABaby=new ABaby();
echo($ABaby->getData(7,77));
$ABabyBed=serialize($ABaby);
$newABaby=unserialize($ABabyBed);
echo($newABaby->getLastData());
注释:
通过增加一个魔法函数__wakeup修正上面的问题。
运行上面的脚本应该一切正常。unserialize()操作过程的最后一步会自动检测类是否定义了__wakeup()魔法函数,有则自动调用。这里面我们通过__wakeup()只是简单的重新打开文件流操作句柄。可以在__wakeup()中加入“echo('ABaby');”证实它确实被自动调用了。
上面的代码存在一个“隐患”。
我们序列化一个对象后,通常意味着近期不会马上使用它,但是上面的代码我们却没有显式的关闭打开的文件流操作句柄。虽然,几乎每门语言都宣称自己的垃圾回收机制可以自动释放掉不用的资源,但是实际并不总是这样。所以及时释放掉不用的资源是个好的习惯。下面我们通过__sleep()魔法函数告诉解析器一旦这个对象被序列化,它应该释放掉占用的句柄。
3、
class ABaby
{
var $fileName,$fileOpen,$offset,$length;
function ABaby(){
$this->fileName='ABaby.class.php';
$this->fileOpen=fopen($this->fileName,'rb');
$this->offset=0;
$this->length=0;
}
function getData($offset,$length){
$this->offset=$offset;
$this->length=$length;
fseek($this->fileOpen,$offset);
return(fread($this->fileOpen,$length));
}
function getLastData(){
return($this->getData($this->offset,$this->length));
}
function __sleep(){
fclose($this->fileOpen);
}
function __wakeup(){
$this->fileOpen=fopen($this->fileName,'rb');
}
}
$ABaby=new ABaby();
echo($ABaby->getData(7,77));
$ABabyBed=serialize($ABaby);
$newABaby=unserialize($ABabyBed);
echo($newABaby->getLastData());
注释:
试着增加__sleep()在序列化后释放掉不再使用的、被遗忘的句柄。
运行上面的脚本。出错了:__sleep()应该返回一个数组,这个属组包含类中的实例变量的名字。这是__sleep()的精致之处:可以告诉解析器哪些变量需要被序列化保存下来。这个属组不包含的实例变量(注意这个数组存放的实际是变量名而非值)将被抛弃,这样可以给序列化产生的字串瘦身、并在反序列化时提速。现在看来,__sleep()可以从两个方面优化系统性能:
1、释放资源;
2、有选择性的序列化实例变量。
修正上面的代码如下:
4、
class ABaby
{
var $fileName,$fileOpen,$offset,$length;
function ABaby(){
$this->fileName='ABaby.class.php';
$this->fileOpen=fopen($this->fileName,'rb');
$this->offset=0;
$this->length=0;
}
function getData($offset,$length){
$this->offset=$offset;
$this->length=$length;
fseek($this->fileOpen,$offset);
return(fread($this->fileOpen,$length));
}
function getLastData(){
return($this->getData($this->offset,$this->length));
}
function __sleep(){
fclose($this->fileOpen);
$arr=array();
array_push($arr,'fileName');
array_push($arr,'offset');
array_push($arr,'length');
return($arr);
}
function __wakeup(){
$this->fileOpen=fopen($this->fileName,'rb');
}
}
$ABaby=new ABaby();
echo($ABaby->getData(7,77));
$ABabyBed=serialize($ABaby);
$newABaby=unserialize($ABabyBed);
echo($newABaby->getLastData());
注释:
修正了__sleep()定义
运行正常,结果符合预期。
最后补充一点有关对象序列化的内容。对象序列化通常被用来保存对象当时的状态以备后用,更适合于跨页、跨时间、跨服务器交换/传递对象格式的数据。
对于跨页传递对象,可能会有人提出直接使用session存放对象,这未尝不可。不过有两个问题值得注意:
1、事实上变量被存放进session时会被自动序列化成字符串再存入;从session中读取对象时,解析器会首先反序列化(unserialize())再返回对象。自然,魔法函数对这个过程也应该是有效的。
2、由于session是会话级变量,所有使用session_start的页面都必须包含类定义的脚本文件,因为在session_start时会自动发序列化(unserialize())存放的对象(前面自动序列化得到的字符串)
这样看来,向session中存放对象要付出一定的代价。所以个人建议是将要保存的对象序列化得到的字符串保存进session,使用时再显式的反序列化出实例对象。
关于魔法函数的说告一段落。
至此,本人在Php对象编程中遇到的几个典型的兼容性问题以及部分心得基本介绍完毕。水平所限,有所疏漏或谬误恳请指正。