PHP是弱类型语言,不需要明确的定义变量的类型,变量的类型根据使用时的上下文所决定,也就是变量会根据不同表达式所需要的类型自动转换,比如求和,PHP会将两个相加的值转为long、double再进行加和。每种类型转为另外一种类型都有固定的规则,当某个操作发现类型不符时就会按照这个规则进行转换,这个规则正是弱类型实现的基础。
除了自动类型转换,PHP还提供了一种强制的转换方式:
- (int)/(integer):转换为整形 integer
- (bool)/(boolean):转换为布尔类型 boolean
- (float)/(double)/(real):转换为浮点型 float
- (string):转换为字符串 string
- (array):转换为数组 array
- (object):转换为对象 object
- (unset):转换为 NULL
无论是自动类型转换还是强制类型转换,不是每种类型都可以转为任意其他类型。
这种转换比较简单,任意类型都可以转为NULL,转换时直接将新的zval类型设置为IS_NULL
即可。
当转换为 boolean 时,根据原值的TRUE、FALSE决定转换后的结果,以下值被认为是 FALSE:
- 布尔值 FALSE 本身
- 整型值 0
- 浮点型值 0.0
- 空字符串,以及字符串 "0"
- 空数组
- NULL
所有其它值都被认为是 TRUE,比如资源、对象(这里指默认情况下,因为可以通过扩展改变这个规则)。
判断一个值是否为true的操作:
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()
一致。
其它类型转为整形的转换规则:
- NULL:转为0
- 布尔型:false转为0,true转为1
- 浮点型:向下取整,比如:
(int)2.8 => 2
- 字符串:就是C语言strtoll()的规则,如果字符串以合法的数值开始,则使用该数值,否则其值为 0(零),合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分
- 数组:很多操作不支持将一个数组自动整形处理,比如:
array() + 2
,将报error错误,但可以强制把数组转为整形,非空数组转为1,空数组转为0,没有其他值 - 对象:与数组类似,很多操作也不支持将对象自动转为整形,但有些操作只会抛一个warning警告,还是会把对象转为1操作的,这个需要看不同操作的处理情况
- 资源:转为分配给这个资源的唯一编号
具体处理:
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;
}
除字符串类型外,其它类型转换规则与整形基本一致,就是整形转换结果加了一位小数,字符串转为浮点数由zend_strtod()
完成,这个函数非常长,定义在zend_strtod.c
中,这里不作说明。
一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串。在一个需要字符串的表达式中,会自动转换为 string,比如在使用函数 echo 或 print 时,或在一个变量和一个 string 进行比较时,就会发生这种转换。
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;
}
如果将一个null、integer、float、string、boolean 和 resource 类型的值转换为数组,将得到一个仅有一个元素的数组,其下标为 0,该元素即为此标量的值。换句话说,(array)$scalarValue 与 array($scalarValue) 完全一样。
如果一个 object 类型转换为 array,则结果为一个数组,数组元素为该对象的全部属性,包括public、private、protected,其中private的属性转换后的key加上了类名作为前缀,protected属性的key加上了"*"作为前缀,但是这个前缀并不是转为数组时单独加上的,而是类编译生成属性zend_property_info时就已经加上了,也就是说这其实是成员属性本身的一个特点,举例来看:
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
)
转换时的处理:
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);
}
如果其它任何类型的值被转换成对象,将会创建一个内置类 stdClass 的实例:如果该值为 NULL,则新的实例为空;array转换成object将以键名成为属性名并具有相对应的值,数值索引的元素也将转为属性,但是无法通过"->"访问,只能遍历获取;对于其他值,会以scalar作为属性名。
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;
}
}
}
无法将其他类型转为资源。