4.1 类型转换

PHP是弱类型语言,不需要明确的定义变量的类型,变量的类型根据使用时的上下文所决定,也就是变量会根据不同表达式所需要的类型自动转换,比如求和,PHP会将两个相加的值转为long、double再进行加和。每种类型转为另外一种类型都有固定的规则,当某个操作发现类型不符时就会按照这个规则进行转换,这个规则正是弱类型实现的基础。

除了自动类型转换,PHP还提供了一种强制的转换方式:

  • (int)/(integer):转换为整形 integer
  • (bool)/(boolean):转换为布尔类型 boolean
  • (float)/(double)/(real):转换为浮点型 float
  • (string):转换为字符串 string
  • (array):转换为数组 array
  • (object):转换为对象 object
  • (unset):转换为 NULL

无论是自动类型转换还是强制类型转换,不是每种类型都可以转为任意其他类型。

4.1.1 转换为NULL

这种转换比较简单,任意类型都可以转为NULL,转换时直接将新的zval类型设置为IS_NULL即可。

4.1.2 转换为布尔型

当转换为 boolean 时,根据原值的TRUE、FALSE决定转换后的结果,以下值被认为是 FALSE:

  • 布尔值 FALSE 本身
  • 整型值 0
  • 浮点型值 0.0
  • 空字符串,以及字符串 “0”
  • 空数组
  • NULL

所有其它值都被认为是 TRUE,比如资源、对象(这里指默认情况下,因为可以通过扩展改变这个规则)。

判断一个值是否为true的操作:

  1. static zend_always_inline int i_zend_is_true(zval *op)
  2. {
  3. int result = 0;
  4. again:
  5. switch (Z_TYPE_P(op)) {
  6. case IS_TRUE:
  7. result = 1;
  8. break;
  9. case IS_LONG:
  10. //非0即真
  11. if (Z_LVAL_P(op)) {
  12. result = 1;
  13. }
  14. break;
  15. case IS_DOUBLE:
  16. if (Z_DVAL_P(op)) {
  17. result = 1;
  18. }
  19. break;
  20. case IS_STRING:
  21. //非空字符串及"0"外都为true
  22. if (Z_STRLEN_P(op) > 1 || (Z_STRLEN_P(op) && Z_STRVAL_P(op)[0] != '0')) {
  23. result = 1;
  24. }
  25. break;
  26. case IS_ARRAY:
  27. //非空数组为true
  28. if (zend_hash_num_elements(Z_ARRVAL_P(op))) {
  29. result = 1;
  30. }
  31. break;
  32. case IS_OBJECT:
  33. //默认情况下始终返回true
  34. result = zend_object_is_true(op);
  35. break;
  36. case IS_RESOURCE:
  37. //合法资源就是true
  38. if (EXPECTED(Z_RES_HANDLE_P(op))) {
  39. result = 1;
  40. }
  41. case IS_REFERENCE:
  42. op = Z_REFVAL_P(op);
  43. goto again;
  44. break;
  45. default:
  46. break;
  47. }
  48. return result;
  49. }

在扩展中可以通过convert_to_boolean()这个函数直接将原zval转为bool型,转换时的判断逻辑与i_zend_is_true()一致。

4.1.3 转换为整型

其它类型转为整形的转换规则:

  • NULL:转为0
  • 布尔型:false转为0,true转为1
  • 浮点型:向下取整,比如:(int)2.8 => 2
  • 字符串:就是C语言strtoll()的规则,如果字符串以合法的数值开始,则使用该数值,否则其值为 0(零),合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分
  • 数组:很多操作不支持将一个数组自动整形处理,比如:array() + 2,将报error错误,但可以强制把数组转为整形,非空数组转为1,空数组转为0,没有其他值
  • 对象:与数组类似,很多操作也不支持将对象自动转为整形,但有些操作只会抛一个warning警告,还是会把对象转为1操作的,这个需要看不同操作的处理情况
  • 资源:转为分配给这个资源的唯一编号

具体处理:

  1. ZEND_API zend_long ZEND_FASTCALL _zval_get_long_func(zval *op)
  2. {
  3. try_again:
  4. switch (Z_TYPE_P(op)) {
  5. case IS_NULL:
  6. case IS_FALSE:
  7. return 0;
  8. case IS_TRUE:
  9. return 1;
  10. case IS_RESOURCE:
  11. //资源将转为zend_resource->handler
  12. return Z_RES_HANDLE_P(op);
  13. case IS_LONG:
  14. return Z_LVAL_P(op);
  15. case IS_DOUBLE:
  16. return zend_dval_to_lval(Z_DVAL_P(op));
  17. case IS_STRING:
  18. //字符串的转换调用C语言的strtoll()处理
  19. return ZEND_STRTOL(Z_STRVAL_P(op), NULL, 10);
  20. case IS_ARRAY:
  21. //根据数组是否为空转为0,1
  22. return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0;
  23. case IS_OBJECT:
  24. {
  25. zval dst;
  26. convert_object_to_type(op, &dst, IS_LONG, convert_to_long);
  27. if (Z_TYPE(dst) == IS_LONG) {
  28. return Z_LVAL(dst);
  29. } else {
  30. //默认情况就是1
  31. return 1;
  32. }
  33. }
  34. case IS_REFERENCE:
  35. op = Z_REFVAL_P(op);
  36. goto try_again;
  37. EMPTY_SWITCH_DEFAULT_CASE()
  38. }
  39. return 0;
  40. }

4.1.4 转换为浮点型

除字符串类型外,其它类型转换规则与整形基本一致,就是整形转换结果加了一位小数,字符串转为浮点数由zend_strtod()完成,这个函数非常长,定义在zend_strtod.c中,这里不作说明。

4.1.5 转换为字符串

一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串。在一个需要字符串的表达式中,会自动转换为 string,比如在使用函数 echo 或 print 时,或在一个变量和一个 string 进行比较时,就会发生这种转换。

  1. ZEND_API zend_string* ZEND_FASTCALL _zval_get_string_func(zval *op)
  2. {
  3. try_again:
  4. switch (Z_TYPE_P(op)) {
  5. case IS_UNDEF:
  6. case IS_NULL:
  7. case IS_FALSE:
  8. //转为空字符串""
  9. return ZSTR_EMPTY_ALLOC();
  10. case IS_TRUE:
  11. //转为"1"
  12. ...
  13. return zend_string_init("1", 1, 0);
  14. case IS_RESOURCE: {
  15. //转为"Resource id #xxx"
  16. ...
  17. len = snprintf(buf, sizeof(buf), "Resource id #" ZEND_LONG_FMT, (zend_long)Z_RES_HANDLE_P(op));
  18. return zend_string_init(buf, len, 0);
  19. }
  20. case IS_LONG: {
  21. return zend_long_to_str(Z_LVAL_P(op));
  22. }
  23. case IS_DOUBLE: {
  24. return zend_strpprintf(0, "%.*G", (int) EG(precision), Z_DVAL_P(op));
  25. }
  26. case IS_ARRAY:
  27. //转为"Array",但是报Notice
  28. zend_error(E_NOTICE, "Array to string conversion");
  29. return zend_string_init("Array", sizeof("Array")-1, 0);
  30. case IS_OBJECT: {
  31. //报Error错误
  32. zval tmp;
  33. ...
  34. zend_error(EG(exception) ? E_ERROR : E_RECOVERABLE_ERROR, "Object of class %s could not be converted to string", ZSTR_VAL(Z_OBJCE_P(op)->name));
  35. return ZSTR_EMPTY_ALLOC();
  36. }
  37. case IS_REFERENCE:
  38. op = Z_REFVAL_P(op);
  39. goto try_again;
  40. case IS_STRING:
  41. return zend_string_copy(Z_STR_P(op));
  42. EMPTY_SWITCH_DEFAULT_CASE()
  43. }
  44. return NULL;
  45. }

4.1.6 转换为数组

如果将一个null、integer、float、string、boolean 和 resource 类型的值转换为数组,将得到一个仅有一个元素的数组,其下标为 0,该元素即为此标量的值。换句话说,(array)$scalarValue 与 array($scalarValue) 完全一样。

如果一个 object 类型转换为 array,则结果为一个数组,数组元素为该对象的全部属性,包括public、private、protected,其中private的属性转换后的key加上了类名作为前缀,protected属性的key加上了”*”作为前缀,但是这个前缀并不是转为数组时单独加上的,而是类编译生成属性zend_property_info时就已经加上了,也就是说这其实是成员属性本身的一个特点,举例来看:

  1. class test {
  2. private $a = 123;
  3. public $b = "bbb";
  4. protected $c = "ccc";
  5. }
  6. $obj = new test;
  7. print_r((array)$obj);
  8. ======================
  9. Array
  10. (
  11. [testa] => 123
  12. [b] => bbb
  13. [*c] => ccc
  14. )

转换时的处理:

  1. ZEND_API void ZEND_FASTCALL convert_to_array(zval *op)
  2. {
  3. try_again:
  4. switch (Z_TYPE_P(op)) {
  5. case IS_ARRAY:
  6. break;
  7. case IS_OBJECT:
  8. ...
  9. if (Z_OBJ_HT_P(op)->get_properties) {
  10. //获取所有属性数组
  11. HashTable *obj_ht = Z_OBJ_HT_P(op)->get_properties(op);
  12. //将数组内容拷贝到新数组
  13. ...
  14. }
  15. case IS_NULL:
  16. ZVAL_NEW_ARR(op);
  17. //转为空数组
  18. zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0);
  19. break;
  20. case IS_REFERENCE:
  21. zend_unwrap_reference(op);
  22. goto try_again;
  23. default:
  24. convert_scalar_to_array(op);
  25. break;
  26. }
  27. }
  28. //其他标量类型转array
  29. static void convert_scalar_to_array(zval *op)
  30. {
  31. zval entry;
  32. ZVAL_COPY_VALUE(&entry, op);
  33. //新分配一个数组,将原值插入数组
  34. ZVAL_NEW_ARR(op);
  35. zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0);
  36. zend_hash_index_add_new(Z_ARRVAL_P(op), 0, &entry);
  37. }

4.1.7 转换为对象

如果其它任何类型的值被转换成对象,将会创建一个内置类 stdClass 的实例:如果该值为 NULL,则新的实例为空;array转换成object将以键名成为属性名并具有相对应的值,数值索引的元素也将转为属性,但是无法通过”->”访问,只能遍历获取;对于其他值,会以scalar作为属性名。

  1. ZEND_API void ZEND_FASTCALL convert_to_object(zval *op)
  2. {
  3. try_again:
  4. switch (Z_TYPE_P(op)) {
  5. case IS_ARRAY:
  6. {
  7. HashTable *ht = Z_ARR_P(op);
  8. ...
  9. //以key为属性名,将数组元素拷贝到对象属性
  10. object_and_properties_init(op, zend_standard_class_def, ht);
  11. break;
  12. }
  13. case IS_OBJECT:
  14. break;
  15. case IS_NULL:
  16. object_init(op);
  17. break;
  18. case IS_REFERENCE:
  19. zend_unwrap_reference(op);
  20. goto try_again;
  21. default: {
  22. zval tmp;
  23. ZVAL_COPY_VALUE(&tmp, op);
  24. object_init(op);
  25. //以scalar作为属性名
  26. zend_hash_str_add_new(Z_OBJPROP_P(op), "scalar", sizeof("scalar")-1, &tmp);
  27. break;
  28. }
  29. }
  30. }

4.1.8 转换为资源

无法将其他类型转为资源。