Php 3.x与4.x中关于对象编程的不兼容问题

类别:编程语言 点击:0 评论:0 推荐:
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中关于对象编程的一些游戏规则改变了,不兼容。笔者就实际开发过程遇到的问题稍作讨论,相信有些问题可能笔者尚未遇到,欢迎诸位补充、共赏。

一、构造器(函数)说
    在3.x中,构造器指的是与类同名的类函数。有点拗口,不过更拗口的还有。在4.x中,构造器指的是在构造器内部直接定义的与类同名的类函数,就是说不包括继承来的类函数。比如:
1、  

    class ABabyStudio
    {
        function ABabyStudio(){
          echo('call constructor'.chr(13));
        }
        //...
    }

注释:
    在3.x和4.x中效果相同
2、

    class ABaby
    {
        function ABabyStudio(){
            echo('ABabyStudio');
        }
    }
    class ABabyStudio
    {
        //...
    }

注释:
    在3.x中类ABabyStudio有构造器,从父类ABaby继承获得;在4.x中将没有构造器。读者可试运行这样一行代码
    $ABabyStudio=new ABabyStudio();
    在3.x版本中应该会输出“ABabyStudio”,而4.x中则不会输出。

    还有一点值得注意的是,在4.x中规定实例化一个对象时,只能为类变量指定常量值(或称作标量值)作为初始值。如果想要通过变量(通常是全局变量)、对象、表达式设定初始值,应该在构造器中进行操作。比如:
3、

    class ABaby
    {
        //...
    }
    class ABabyStudio
    {
        var $ababy='hello every one';    //sentence 1
        var $ababy='hello'.' world ';    //sentence 2
        var $ababy=new ABaby();          //sentence 3
        var $ababy=array();              //sentence 4
        var $ababy=g_hello;              //sentence 5,g_hello is a constant
        function ABabyStudio(){
          echo('call constructor'.chr(13));
        }
        //...
    }

注释:
    5个标示的sentence只有1、4、5可以在4.x下通过。不过,笔者没有了解Php工作组为什么让sentence 4通过,只能强迫自己理解为:array被作为一种基本数据类型,array()不被看作方法或表达式。

4、

    class ABaby
    {
        //...
    }
    class ABabyStudio
    {
        var $ababy;
        function ABabyStudio(){
          $this->ababy='hello every one';
          $this->ababy='hello'.' every'.' one';
          $this->ababy=new ABaby();
          $this->ababy=array();
          $this->ababy=g_hello;
          echo('call constructor'.chr(13));
        }
        //...
    }

注释:
    这段代码在3.x和4.x都能够编译通过,效果相同。
    在3.x和4.x中,关于构造器规则的变化的说明驻足于此。下一篇将介绍按引用传递参数的不兼容问题。

二、按引用传递参数说
    这一节讨论不止适用于Php对象编程,适用于Php编程的所有方面。
    Php中的参数默认是按值传递的,不论是基本数据类型还是对象。和其他面向对象编程语言有所区别的是,通常对象在其他语言中的默认传递方式是按引用传递。为了达到按引用传递参数的目的,Php小组引入了一个“&”操作符来声明当前这个变量应该按引用传递。看起来感觉有点怪怪的。
    这里不讨论基本数据类型。
1、

    class ABabyStudio
    {
      function ABabyStudio(){
        echo('call constructor'.chr(13));
      }
      //...
    }
    $ABabyStudio1 = new ABabyStudio();
    $ABabyStudio2 = $ABabyStudio1;
    $ABabyStudio3 = & $ABabyStudio1;

注释:
    $ABabyStudio2获得了一个$ABabyStudio1的复制品,而不是$ABabyStudio1本身,就是说以后1变化并不会影响到2,反之亦成立。
    $ABabyStudio3将得到$ABabyStudio1的引用,1和3的变化是同步的。

2、

    class ABaby
    {
      var $hello;
      function ABaby(){
        $this->hello=' world';
      }
    //...
    }
    class ABabyStudio
    {
      function ABabyStudio(){
        ;
      }
      function helloEveryOne($ABaby){
          $ABaby->hello=' every one';
      }
      //...
    }
    $ABaby=new ABaby;
    echo($ABaby->hello);
    $ABabyStudio=new ABabyStudio();
    $ABabyStudio->helloEveryOne(& $ABaby);
    echo($ABabyStudio->hello);

注释:
    你在3.x中运行这段代码,一切OK。换到4.x环境中,问题出现了:
    PHP Warning:  Call-time pass-by-reference has been deprecated - argument passed by value;
    If you would like to pass it by reference, modify the declaration of [runtime function name]().
    If you would like to enable call-time pass-by-reference, you can set
    allow_call_time_pass_reference to true in your INI file.
    However, future versions may not support this any longer.

    在4.x中,Php向通用模式靠拢。如果想要向一个函数按引用传递参数,应该在函数声明中定义,具体形式就是在形参前加上引用操作符“&”:

    function yourFunction(& $arg){
      $arg++;
    }
    $arg=7;
    yourFunction($arg);
    echo($arg);

    运行这段代码,你将发现$arg的值递增到“8”。所以,代码段2应修改为:
3、

    class ABaby
    {
      var $hello;
      function ABaby(){
        $this->hello=' world';
      }
    //...
    }
    class ABabyStudio
    {
      function ABabyStudio(){
        ;
      }
      function helloEveryOne(& $ABaby){
          $ABaby->hello=' every one';
      }
      //...
    }
    $ABaby=new ABaby;
    echo($ABaby->hello);
    $ABabyStudio=new ABabyStudio();
    $ABabyStudio->helloEveryOne($ABaby);
    echo($ABabyStudio->hello);

注释:
    无。

    类似的,如果想要函数返回一个“引用”而不是“拷贝”,同样可以在函数声明中实现。这在3.x中是不可能的:

    function & yourFunction(&$arg){
      $arg++;
      return($arg);
    }
    $myArg=7;
    $argRef=& yourFunction($myArg);
    $argRef+=10;
    echo($myArg);

    运行这段代码将发现$arg的值经过两次加法运算变为18。可能有人质疑这一句“$argRef=& yourFunction($arg);”既然函数已经按引用传递回了$arg,为什么还要在这一句中使用“&”?问的好!这是因为,声明函数按引用返回参数$arg后,只是告诉函数(或者Php解析器)在return时不要把$arg拷贝一份送出来,而由于函数体返回值(实际是一个引用)要经历一次赋只“=”操作,所以要函数前面再次出现“&”。这样才保证了内存中自始至终只存在一个$myArg,$myArg的值才会被两次改变。
    如果上面的$myArg是一个对象,上面的陈述同样适用。读者可以自己尝试一下。

    补充说明一点,依然可以强迫目前的4.x版本支持3.x中的传递引用的方式,可以通过修改php.ini将下面一行:
    allow_call_time_pass_reference = Off
    改为:
    allow_call_time_pass_reference = On
    不过,并不保证在后续的版本中继续支持这种方式。
    停笔观望,欢迎大家进一步讨论。下一篇介绍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对象编程中遇到的几个典型的兼容性问题以及部分心得基本介绍完毕。水平所限,有所疏漏或谬误恳请指正。

本文地址:http://com.8s8s.com/it/it29210.htm