PHP的Typed Properties内部实现:在Zval头中存储类型信息与运行时类型检查

PHP Typed Properties 内部实现:Zval 头中的类型信息与运行时类型检查

大家好,今天我们来深入探讨 PHP Typed Properties 的内部实现机制,重点关注类型信息在 Zval 头中的存储方式以及 PHP 如何在运行时进行类型检查。

一、Typed Properties 的引入与意义

在 PHP 7.4 之前,PHP 的类属性声明非常灵活,允许任何类型的变量赋值给任何属性,这虽然带来了开发的便利性,但也导致了类型错误难以在早期发现,增加了调试的难度。PHP 7.4 引入了 Typed Properties,允许我们在类属性声明时指定类型,从而可以在编译时和运行时进行类型检查,提升代码的健壮性和可维护性。

例如:

class MyClass {
    public int $id;
    public string $name;
    public ?float $price; // 允许为 null
}

$obj = new MyClass();
$obj->id = 123;
$obj->name = "Example";
$obj->price = 3.14;

// 如果尝试赋值其他类型,将抛出 TypeError 异常
// $obj->id = "abc"; // TypeError

Typed Properties 的引入,使得 PHP 的类型系统更加完善,更接近于强类型语言,这对于大型项目的开发和团队协作非常有益。

二、Zval 结构体:PHP 变量的核心

要理解 Typed Properties 的内部实现,首先需要了解 Zval 结构体。Zval 是 PHP 引擎中用于表示变量的核心数据结构,所有的 PHP 变量都以 Zval 的形式存在。在 PHP 7 及更高版本中,Zval 的结构如下(简化版):

typedef struct _zval_struct {
    zend_value        value;      /* 变量的值 */
    zend_uchar        type;       /* 变量的类型 */
    zend_uchar        is_refcounted; /* 是否是引用计数变量 */
} zval;

typedef union _zend_value {
    zend_long         lval;       /* long 类型的整数 */
    double            dval;       /* double 类型的浮点数 */
    zend_string      *str;       /* 字符串 */
    zend_array       *arr;       /* 数组 */
    zend_object      *obj;       /* 对象 */
    zend_resource    *res;       /* 资源 */
    zend_reference   *ref;       /* 引用 */
    zend_ast          *ast;       /* 抽象语法树节点 */
    zval             *zv;       /* zval 类型的指针,用于递归 */
    void             *ptr;       /* 通用指针 */
    zend_class_entry *ce;       /* 类入口 */
    zend_function    *func;      /* 函数 */
    struct {
        uint32_t w1;
        uint32_t w2;
    } counted;
} zend_value;
  • value: 这是一个联合体 (zend_value),用于存储变量的实际值。 根据 type 的不同,value 会存储不同类型的数据,如整数、浮点数、字符串、数组、对象等。
  • type: 这是一个无符号字符 (zend_uchar),用于表示变量的类型。 它存储了 IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT 等常量,指明了 value 联合体中哪个成员是有效的。
  • is_refcounted: 这是一个无符号字符 (zend_uchar),用于表示该变量是否是引用计数变量。 如果该变量是引用计数的,那么它的 value 中存储的数据(例如字符串、数组、对象)会被共享,当引用计数降为 0 时,才会释放内存。

三、Typed Properties 与 Zval:类型信息的存储

当声明 Typed Properties 时,PHP 需要将属性的类型信息存储在某个地方,以便在运行时进行类型检查。这个类型信息并没有直接存储在 Zval 结构体中,而是存储在 zend_property_info 结构体中,该结构体与类定义关联,并包含关于每个属性的信息。

zend_property_info 结构体的定义(简化版)如下:

typedef struct _zend_property_info {
    zend_string *name;
    zend_class_entry *ce;
    zend_long offset;
    zend_uchar flags;
    zend_type type; // 新增的类型信息
} zend_property_info;

typedef struct _zend_type {
    zend_uchar type;   // 类型ID,例如 IS_LONG, IS_STRING
    zend_bool  allows_null; // 是否允许为 null
} zend_type;
  • name: 属性的名称。
  • ce: 属性所属的类。
  • offset: 属性在对象中的偏移量。
  • flags: 属性的标志,例如是否是静态属性、是否是受保护的属性等。
  • type: 这是一个 zend_type 结构体,用于存储属性的类型信息。zend_type 结构体包含两个成员:
    • type: 类型 ID,例如 IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT 等。
    • allows_null: 一个布尔值,表示该属性是否允许为 null

因此,Typed Properties 的类型信息存储在 zend_property_info 结构体的 type 成员中,而 zend_property_info 结构体与类定义关联。当访问 Typed Properties 时,PHP 会从 zend_property_info 结构体中获取类型信息,并与要赋的值的类型进行比较,以进行类型检查。

四、运行时类型检查的实现

当尝试给 Typed Properties 赋值时,PHP 会进行运行时类型检查。这个过程大致如下:

  1. 获取属性的 zend_property_info: PHP 首先需要找到要赋值的属性的 zend_property_info 结构体。这通常通过属性的名称和所属的类来完成。
  2. 获取属性的类型信息: 从 zend_property_info 结构体中获取 zend_type,其中包含了属性的类型 ID 和是否允许为 null 的信息。
  3. 获取要赋值的值的类型: PHP 使用 Z_TYPE(zval) 宏来获取要赋值的值的 Zval 的类型。
  4. 进行类型比较: 将要赋值的值的类型与属性的类型进行比较。
    • 如果类型匹配,或者要赋值的值是 null 且属性允许为 null,则赋值成功。
    • 如果类型不匹配,且要赋值的值不是 null,则抛出 TypeError 异常。
  5. 赋值: 如果类型检查通过,则将值赋给属性。

下面是一个简化的代码片段,演示了运行时类型检查的过程:

// 假设已经获取了属性的 zend_property_info 和要赋值的值的 zval
zend_property_info *prop_info = ...;
zval *value = ...;

zend_uchar expected_type = prop_info->type.type;
zend_bool allows_null = prop_info->type.allows_null;
zend_uchar actual_type = Z_TYPE_P(value);

if (actual_type == expected_type || (actual_type == IS_NULL && allows_null)) {
    // 类型匹配,可以赋值
    // ...
} else {
    // 类型不匹配,抛出 TypeError 异常
    zend_throw_exception_ex(zend_ce_type_error, 0, "Typed property %s::$%s must be %s, %s used",
        ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(prop_info->name),
        zend_get_type_by_const(expected_type), zend_get_type_by_const(actual_type));
}

这个代码片段的核心逻辑是比较 actual_typeexpected_type。如果它们相等,或者 actual_typeIS_NULLallows_null 为真,则认为类型匹配。否则,抛出 TypeError 异常。

五、类型转换与强制类型转换

PHP 允许进行一些类型转换,例如将整数转换为浮点数,或者将字符串转换为整数。当给 Typed Properties 赋值时,PHP 也会尝试进行类型转换。如果类型转换成功,则赋值成功。如果类型转换失败,则抛出 TypeError 异常。

例如:

class MyClass {
    public float $price;
}

$obj = new MyClass();
$obj->price = 123; // 整数 123 会被转换为浮点数 123.0
$obj->price = "3.14"; // 字符串 "3.14" 会被转换为浮点数 3.14

在内部实现上,PHP 使用 convert_to_*() 系列函数来进行类型转换。例如,convert_to_long() 用于将变量转换为整数,convert_to_double() 用于将变量转换为浮点数,convert_to_string() 用于将变量转换为字符串。

需要注意的是,PHP 的类型转换规则比较复杂,有时可能会产生意想不到的结果。因此,建议在编写代码时尽量避免隐式类型转换,而是使用显式类型转换,以提高代码的可读性和可维护性。

例如:

class MyClass {
    public int $id;
}

$obj = new MyClass();
$obj->id = (int) "123"; // 显式类型转换

六、联合类型 (Union Types)

PHP 8.0 引入了联合类型,允许属性声明为多个类型中的一种。 例如:

class MyClass {
    public int|string $id;
}

$obj = new MyClass();
$obj->id = 123;
$obj->id = "abc";

在内部实现上,联合类型的信息仍然存储在 zend_property_info 结构体的 type 成员中,但 zend_type 结构体需要进行扩展,以支持存储多个类型的信息。一种可能的实现方式是使用位掩码来表示多个类型,例如:

typedef struct _zend_type {
    uint32_t type_mask; // 使用位掩码表示多个类型
    zend_bool  allows_null;
} zend_type;

#define IS_TYPE_LONG   (1 << 0)
#define IS_TYPE_STRING (1 << 1)

class MyClass {
    public int|string $id; // type_mask = IS_TYPE_LONG | IS_TYPE_STRING
}

在运行时类型检查时,需要检查要赋值的值的类型是否在类型掩码中。

if ((actual_type & expected_type_mask) || (actual_type == IS_NULL && allows_null)) {
    // 类型匹配
} else {
    // 类型不匹配
}

七、总结:类型信息存储与运行时检查

Typed Properties 的引入,使得 PHP 的类型系统更加完善,提升了代码的健壮性和可维护性。类型信息存储在 zend_property_info 结构体中,该结构体与类定义关联。运行时类型检查通过比较要赋值的值的类型与属性的类型来实现。PHP 允许进行一些类型转换,但建议尽量避免隐式类型转换,而是使用显式类型转换。联合类型的引入进一步增强了 PHP 的类型系统的表达能力。理解 Typed Properties 的内部实现机制,有助于我们编写更加健壮和可维护的 PHP 代码。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注