月度归档:2011年05月

理解PHP中的stdClass类

在百度百科中,对于stdClass的定义如下:

stdClass在PHP5才开始被流行。而stdClass也是zend的一个保留类。stdClass是PHP的一个基类,
所有的类几乎都继承这个类,所以任何时候都可以被new,可以让这个变量成为一个object。同时,
这个基类又有一个特殊的地方,就是没有方法。凡是用new stdClass()的变量,
都不可能会出现$a->test()这种方式的使用。PHP5的对象的独特性,对象在任何地方被调用,
都是引用地址型的,所以相对消耗的资源会少一点。在其它页面为它赋值时是直接修改,而不是引用一个拷贝。

以上的定义大多数都是正确的,但是一个致命性的诊断错误: stdClass是PHP的一个基类,所有的类几乎都继承这个类。 看一个简单的例子:

class EmptyClass {

}

$object = new EmptyClass();
if ($object instanceof stdClass) {
    echo 'yes';
}else{
    echo 'no';
}

执行代码,输出”no”,这个例子充分说明了stdClass类并不是所有类的基类。它仅仅是PHP的一个保留类,或者说一个类似于strlen函数这样的一个角色。 我们从源码的维度看看stdClass类的实现,它注册的位置在 Zend/zend_builtin_functions.c文件中。如下:

ZEND_MINIT_FUNCTION(core) { /* {{{ */
    zend_class_entry class_entry;

    /* 注册stdClass 类 */
    INIT_CLASS_ENTRY(class_entry, "stdClass", NULL);
    zend_standard_class_def = zend_register_internal_class(&class_entry TSRMLS_CC);

    /* 注册默认类,接口,如Exception类,SPL中的一些类等 */
    zend_register_default_classes(TSRMLS_C);    

    return SUCCESS;
}
/* }}} */

这是zend_builtin_module的模块初始化函数,在PHP内核进行模块初始化操作时会自动加载这个函数, 这样,stdClass类的注册操作也就会被执行了。从这段代码可以看出,stdClass类是一个没有成员变量也没有成员方法的类。 它的所有的魔术方法,父类、接口等在初始化时都被设置成NULL。由于在PHP中对于一个类我们无法动态的添加方法, 所以这个类只能用来处理动态属性,这也是我们一种常见的用法。

总结一下:

stdClass类是PHP的一个内部保留类,初始时没有成员变量也没成员方法,所有的魔术方法都被设置为NULL,可以使用其传递变量参数,但是没有可以调用的方法。stdClass类可以被继承,只是这样做没有什么意义。

思考PHP之五:访问控制

面向对象三大特性:封装性、继承性和多态性。 封装隐藏了对象内部的细节和实现, 使对象能够集中而完整的描述并对应一个具体的事物。 它使对象只提供对外的访问接口,这样可以在不改变接口的前提下改变实现细节,而且能使对象自我完备。 除此之外,封装还可以增强安全性和简化编程。 继承的目的是为了实现代码复用,它是一种一般化与特殊化的关系,其中子类是父类的细化。 在实现继承时最需要考虑的问题是子类和父类是不是”IS-A”的关系。

PHP(其它面向对象的语言也类似)对于封装和继承的一些特性是通过访问控制实现。访问控制的作用是控制成员变量,成员方法和基类。 曾经一直以为访问控制的作用仅仅是控制一个类的成员方法和成员变量,这把自己的思维局限于一类一对象了, 这两个方面的控制是PHP对面向对象中封装特性的支持。 把思维拉升到面向对象的体系之上,访问控制也控制了基类(或父类)的行为,或者说控制了继承特性的某些方面。

PHP中关于访问控制的关键字和Java等其它面向对象语言一样,如下:

  • public 所定义的类成员可以在任何地方被访问
  • protected 所定义的类成员则可以被其所在类、其所在类的子类和父类访问
  • private 定义的类成员则只能被其所在类访问。

以上的类成员包括成员变量和成员函数。不管是成员变量还是成员方法,PHP默认都是public。 在Java中访问控制默认为包可见,在C++中访问控制默认为私有(private),而PHP则是公有的(public),这比Java还要open。 笔者认为这是PHP的一个历史遗留问题。如果可以重新设计PHP,可能是另一个结果,并且这也是语言的对于访问的态度问题。

前面介绍的各个访问控制是针对封装性,对于继承性,如下:

  • public/protected 可以被继承
  • private 没有被继承

实际上,在PHP中,私有方法也会被继承下来,只是其上下文没有改变(还是父类),从而在调用的时候出错。

一般来说,private定义的成员只能被内部调用,仅供当前类使用,这在PHP的源码中检查访问权限控制时, 以private的成员会检查是否属于当前类体现。public定义的成员则属于类或对象的外部接口, 声明的public成员最好是定义好后就不要再变更,这会影响到调用了类的这些方法的相关客户。 好的public和private的设计对于对象本身的自我完备的实际有重大的意义。

但是public关键字有一些二义性。对于封装性,它是公有的,任何地方都可以访问的成员;对于继承性, 它允许子类继承此成员。同时兼顾这两个特性,当我们把它作为一个接口提供给外部使用时就会有一些歧义: 子类可以覆盖该成员方法,同时也可以调用访方法,如果子类覆盖了该成员方法并调用了该方法, 则它的实现就和你当初作为接口提供给外部时的含义有一些不同了。和public一样,protected也有类似的问题。 可以思考一下:各语言这样实现的目的是什么?是否有更好的方案?

PHP中的前缀自增(++i) 和后缀自增 (i++)

当我们学第一门语言时,比如大学课程中的C语言程序设计,也许曾经被前缀自增(++i) 和后缀自增 (i++)纠结过。 曾经以为我们懂了:

  • i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1
  • ++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值

这个表达基本没错,只能说不够精确。在《Expert C Programming》这本书中的附录中,有这样一段说明: ++i表示取i的地址,增加它的内容,然后把值放在寄存器中;i++表示取i的地址,把它的值装入寄存器中,然后增加内存中的i的值。 这里的寄存器存放的就是我们在表达式中使用的值。

在PHP中也有++$i和$i++,那么Zend内核是如何实现这两种自增方式的呢? 看下面一个例子,在不运行这段代码的情况下,你认为会输出什么呢?

$i = 0;
$i = $i++;
echo $i;

咱们先不论答案是什么?我们直接从Zend内核查看这种自增操作的实现。

使用VLD查看包含了$i++和++$i的PHP代码生成的中间代码:

$i = 0;
$i++;
++$i;

使用VLD命令(php -dvld.active=1 -dvld.verbosity=3 t.php)查看详细参数:

number of ops:  8
compiled vars:  !0 = $i
line     # *  op                           fetch          ext  return  operands
--------------------------------------------------------------------------------
-
   2     0  >   EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         1      ASSIGN                                                    OP1[IS_CV !0 ] OP2[ ,  IS_CONST (0) 0 ]
   3     2      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         3      POST_INC                                          RES[  IS_TMP_VAR ~1 ]       OP1[  IS_CV !0 ]
         4      FREE                                                      OP1[IS_TMP_VAR ~1 ]
   4     5      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         6      PRE_INC                                                   OP1[IS_CV !0 ]
   5     7    > RETURN                                                    OP1[IS_CONST (0) 1 ]

branch: #  0; line:     2-    5; sop:     0; eop:     7
path #1: 0,

从VLD扩展的输出信息可以知道,前缀自增(++$i)对应的opcode为PRE_INC,后缀自增($i++)对应的opcode为POST_INC。 首先我们看前缀自增(++$i),++$i没有返回值或者说它的返回值为空。 根据中间代码和VLD显示的OP1的参数类型, 我们可以知道++$i的中间代码在执行是最终调用的是Zend/zend_vm_execute.h文件中的ZEND_PRE_INC_SPEC_CV_HANDLER函数。 在ZEND_PRE_INC_SPEC_CV_HANDLER函数中有几个关键点:

  • CV类型变量的获取,它是调用_get_zval_ptr_ptr_cv获取CV类型变量。 这里的CV类型的变量是PHP编译期间的类似于缓存的作用,主要作用是提高某些变量的存储速度。
  • increment_function函数,不管是实例变量,类变量或者常规的变量,最终都是调用increment_function函数实现变量的增加操作。 在这个函数中,程序会根据变量的类型做出不同的处理,在PHP5.3.1这个版本中,PHP支持IS_LONG、IS_DOUBLE、IS_NULL和IS_STRING四种类型。 如果变量的类型是IS_NULL,程序会将变量的值赋值为1。如果变量类型是字符串,程序会将其转化成整形或浮点型进行计算。
  • 使用RETURN_VALUE_UNUSED宏清除返回结果,这个宏的作用是将result变量的类型设置为EXT_TYPE_UNUSED类型。

前缀自增(++$i)操作在Zend内核中本质上是操作变量本身,而且在表达式中使用的也是这个变量本身。

了解了++$i的实现,我们来看下可能使用得更多的$i++操作的实现。 同样,从中间代码POST_INC和OP1的类型是IS_CV,我们可以在Zend/zend_vm_execute.h文件中找到其实现为ZEND_POST_INC_SPEC_CV_HANDLER。 与前面的ZEND_PRE_INC_SPEC_CV_HANDLER相比,它们都有一个取CV类型变量的过程,也有一个increment_function函数增加变量值的过程, 但是除此之外它多了一个操作,同时也少了一个操作。 它多的一个操作是:

EX_T(opline->result.u.var).tmp_var = **var_ptr;
zendi_zval_copy_ctor(EX_T(opline->result.u.var).tmp_var);

这两行代码的作用是初始化返回值到临时变量,并且将原始的$i的值存储在这,这就是我们在前面使用VLD查看生成的中间代码其结果为RES[ IS_TMP_VAR ~1 ]的原因。 在这个初始化完成后,程序会继续执行增加操作,在增加操作完成后,它就结束了,而之前的++$i操作则会将result设置为UNUSED类型,这就是它少的那个操作。

后缀自增($i++)在表达式中使用的是存放在临时变量中原先的变量值,而变量本身的值已经增加了。 在PHP中这种变量的分离是通过临时变量+返回值解决。

到这里,我们可以回答最开始的问题了,它会输出0。因为在表达式中$i++的返回值是一个临时变量,也就是$i原来的值,也就是0。