## 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的操作: ```c static zend_always_inline int i_zend_is_true(zval *op) { int result = 0; again: switch (Z_TYPE_P(op)) { case IS_TRUE: result = 1; break; case IS_LONG: //非0即真 if (Z_LVAL_P(op)) { result = 1; } break; case IS_DOUBLE: if (Z_DVAL_P(op)) { result = 1; } break; case IS_STRING: //非空字符串及"0"外都为true if (Z_STRLEN_P(op) > 1 || (Z_STRLEN_P(op) && Z_STRVAL_P(op)[0] != '0')) { result = 1; } break; case IS_ARRAY: //非空数组为true if (zend_hash_num_elements(Z_ARRVAL_P(op))) { result = 1; } break; case IS_OBJECT: //默认情况下始终返回true result = zend_object_is_true(op); break; case IS_RESOURCE: //合法资源就是true if (EXPECTED(Z_RES_HANDLE_P(op))) { result = 1; } case IS_REFERENCE: op = Z_REFVAL_P(op); goto again; break; default: break; } return result; } ``` 在扩展中可以通过`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操作的,这个需要看不同操作的处理情况 * 资源:转为分配给这个资源的唯一编号 具体处理: ```c ZEND_API zend_long ZEND_FASTCALL _zval_get_long_func(zval *op) { try_again: switch (Z_TYPE_P(op)) { case IS_NULL: case IS_FALSE: return 0; case IS_TRUE: return 1; case IS_RESOURCE: //资源将转为zend_resource->handler return Z_RES_HANDLE_P(op); case IS_LONG: return Z_LVAL_P(op); case IS_DOUBLE: return zend_dval_to_lval(Z_DVAL_P(op)); case IS_STRING: //字符串的转换调用C语言的strtoll()处理 return ZEND_STRTOL(Z_STRVAL_P(op), NULL, 10); case IS_ARRAY: //根据数组是否为空转为0,1 return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0; case IS_OBJECT: { zval dst; convert_object_to_type(op, &dst, IS_LONG, convert_to_long); if (Z_TYPE(dst) == IS_LONG) { return Z_LVAL(dst); } else { //默认情况就是1 return 1; } } case IS_REFERENCE: op = Z_REFVAL_P(op); goto try_again; EMPTY_SWITCH_DEFAULT_CASE() } return 0; } ``` ### 4.1.4 转换为浮点型 除字符串类型外,其它类型转换规则与整形基本一致,就是整形转换结果加了一位小数,字符串转为浮点数由`zend_strtod()`完成,这个函数非常长,定义在`zend_strtod.c`中,这里不作说明。 ### 4.1.5 转换为字符串 一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串。在一个需要字符串的表达式中,会自动转换为 string,比如在使用函数 echo 或 print 时,或在一个变量和一个 string 进行比较时,就会发生这种转换。 ```c ZEND_API zend_string* ZEND_FASTCALL _zval_get_string_func(zval *op) { try_again: switch (Z_TYPE_P(op)) { case IS_UNDEF: case IS_NULL: case IS_FALSE: //转为空字符串"" return ZSTR_EMPTY_ALLOC(); case IS_TRUE: //转为"1" ... return zend_string_init("1", 1, 0); case IS_RESOURCE: { //转为"Resource id #xxx" ... len = snprintf(buf, sizeof(buf), "Resource id #" ZEND_LONG_FMT, (zend_long)Z_RES_HANDLE_P(op)); return zend_string_init(buf, len, 0); } case IS_LONG: { return zend_long_to_str(Z_LVAL_P(op)); } case IS_DOUBLE: { return zend_strpprintf(0, "%.*G", (int) EG(precision), Z_DVAL_P(op)); } case IS_ARRAY: //转为"Array",但是报Notice zend_error(E_NOTICE, "Array to string conversion"); return zend_string_init("Array", sizeof("Array")-1, 0); case IS_OBJECT: { //报Error错误 zval tmp; ... 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)); return ZSTR_EMPTY_ALLOC(); } case IS_REFERENCE: op = Z_REFVAL_P(op); goto try_again; case IS_STRING: return zend_string_copy(Z_STR_P(op)); EMPTY_SWITCH_DEFAULT_CASE() } return NULL; } ``` ### 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时就已经加上了,也就是说这其实是成员属性本身的一个特点,举例来看: ```c class test { private $a = 123; public $b = "bbb"; protected $c = "ccc"; } $obj = new test; print_r((array)$obj); ====================== Array ( [testa] => 123 [b] => bbb [*c] => ccc ) ``` 转换时的处理: ```c ZEND_API void ZEND_FASTCALL convert_to_array(zval *op) { try_again: switch (Z_TYPE_P(op)) { case IS_ARRAY: break; case IS_OBJECT: ... if (Z_OBJ_HT_P(op)->get_properties) { //获取所有属性数组 HashTable *obj_ht = Z_OBJ_HT_P(op)->get_properties(op); //将数组内容拷贝到新数组 ... } case IS_NULL: ZVAL_NEW_ARR(op); //转为空数组 zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0); break; case IS_REFERENCE: zend_unwrap_reference(op); goto try_again; default: convert_scalar_to_array(op); break; } } //其他标量类型转array static void convert_scalar_to_array(zval *op) { zval entry; ZVAL_COPY_VALUE(&entry, op); //新分配一个数组,将原值插入数组 ZVAL_NEW_ARR(op); zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0); zend_hash_index_add_new(Z_ARRVAL_P(op), 0, &entry); } ``` ### 4.1.7 转换为对象 如果其它任何类型的值被转换成对象,将会创建一个内置类 stdClass 的实例:如果该值为 NULL,则新的实例为空;array转换成object将以键名成为属性名并具有相对应的值,数值索引的元素也将转为属性,但是无法通过"->"访问,只能遍历获取;对于其他值,会以scalar作为属性名。 ```c ZEND_API void ZEND_FASTCALL convert_to_object(zval *op) { try_again: switch (Z_TYPE_P(op)) { case IS_ARRAY: { HashTable *ht = Z_ARR_P(op); ... //以key为属性名,将数组元素拷贝到对象属性 object_and_properties_init(op, zend_standard_class_def, ht); break; } case IS_OBJECT: break; case IS_NULL: object_init(op); break; case IS_REFERENCE: zend_unwrap_reference(op); goto try_again; default: { zval tmp; ZVAL_COPY_VALUE(&tmp, op); object_init(op); //以scalar作为属性名 zend_hash_str_add_new(Z_OBJPROP_P(op), "scalar", sizeof("scalar")-1, &tmp); break; } } } ``` ### 4.1.8 转换为资源 无法将其他类型转为资源。