数据类型

解释 PHP 的运行时类型、类型声明、强制转换与严格模式,并展示如何在外部输入边界保住类型与业务含义。

难度 入门 时长 标准深度约 12分钟
版本 PHP 8.3.33
what

PHP 的类型属于值;变量可以在运行过程中改为保存另一种类型的值。类型声明则约束参数、返回值、属性和类常量等位置。

trap

强制转换与宽松比较会丢失信息,例如缺失值、非法整数和合法的 0 可能被压成同一个结果。

fix

在输入边界先验证再转换,业务代码使用精确声明与 ===,并在接收 mixed 后立即缩小类型范围。

是什么,为什么存在

PHP 是动态类型语言:值有运行时类型,但变量名不会永久绑定某一种类型。$value 可以先保存整数,随后保存字符串;每次表达式求值时,当前值的类型决定允许的操作与转换规则。这种灵活性适合处理表单、JSON 和数据库结果,也会把一部分错误推迟到代码真正执行时。

PHP 还支持参数、返回值、属性和类常量的类型声明。声明把函数或对象边界允许的值集合写进语言,由运行时在相应位置检查。动态类型与类型声明并不矛盾:前者描述变量和值如何运行,后者为选定边界建立契约。

PHP 值常见的运行时类型如下。get_debug_type() 会报告当前值的实用类型名称,对对象通常返回类名,对资源还会给出资源种类。

类别运行时类型典型用途
标量类型(scalar type) boolintfloatstring单个布尔值、数字或文本
复合值arrayobject容器与带行为的对象
特殊值nullresource缺少值或外部句柄

callableiterable 更像能力集合,而不是 get_debug_type() 会报告的独立值类型。数组和 Traversable 对象都能满足 iterable;闭包、函数名字符串和合法的方法数组都可能满足 callablemixedvoidnever 也主要用于声明:它们分别表示任何值、正常返回但不返回值,以及永不正常返回。

数据类型解决的是「这个位置允许什么值,以及这些值能做什么」。类型本身不会证明字符串来自可信来源,也不会证明整数在业务范围内。int $quantity 能排除数组,却不能排除 -5;边界验证仍然属于应用代码。

工作原理

值携带运行时类型

赋值把一个值绑定到变量名,重新赋值可以改变该名称当前关联值的类型。运算符会根据操作数类型选择行为:. 执行字符串拼接,算术运算需要数值,条件位置会把值解释为布尔值。

检查类型时,优先使用表达意图的函数,例如 is_int()is_string()is_array()is_object()is_iterable()is_callable()get_debug_type() 适合诊断和错误消息。gettype() 返回的部分传统名称与声明语法不同,例如整数会报告为 integer,所以不应把它的字符串结果当作类型系统接口。

同一种表面赋值对不同值有不同复制语义。标量和数组表现为值;修改副本不会修改原变量。对象赋值复制的是指向同一对象的句柄,因此两个变量仍能观察到同一对象的属性变化。显式引用运算符 & 还能让变量成为别名,这与对象句柄不是同一机制。

声明约束边界

原子声明可以是 intstringarray、类名或接口名等。 联合类型(union type) int|string 接受任一成员; 交集类型(intersection type) Countable&Iterator 要求对象同时满足所有成员。单一类型前的 ? 是可空简写,所以 ?string 等价于 string|null

参数在函数进入时检查,返回值在返回时检查,带类型的属性在赋值时检查。PHP 8.3 还允许给类、接口、Trait 与枚举常量声明类型。声明失败通常抛出 TypeError,但具体是否先发生标量强制转换取决于调用位置的严格模式。

声明位置检查时机
函数参数进入调用时
返回类型函数返回时
对象或静态属性给属性赋值时
类常量类定义被处理时

这些检查约束的是声明位置,不会冻结变量或递归检查容器内容。把类型放在稳定的模块边界上,通常比给临时局部变量补充强制转换更有价值。

array 声明只证明值是数组,不证明其中有哪些键或元素类型。callable 也不能在原生声明中写出参数与返回签名,而且不能作为属性类型。需要更细的数组形状、泛型集合或可调用签名时,可以补充 PHPDoc 和静态分析,但运行时验证仍要单独设计。

转换由上下文触发

类型杂耍(type juggling) 是 PHP 根据上下文自动转换值的行为。可转换的数字字符串参加算术时会成为数字,整数与字符串用 . 连接时会按字符串处理,条件表达式则执行布尔转换。显式 (int)(string) 等转换使用相应的转换规则,但「成功得到目标类型」不等于「输入有效」。

布尔上下文尤其容易隐藏业务差别。false、整数 0、浮点 0.0、空字符串、字符串 '0'、空数组和 null 都会被视为假;字符串 '00' 与非空数组则为真。empty() 采用相近的宽泛规则,因此不适合验证必须保留零值的字段。

== 会按比较规则转换操作数,=== 同时要求类型和值一致。宽松比较不是简单的「先全部转成字符串」或「先全部转成数字」,不同类型组合有各自规则。业务标识、状态码和验证结果通常应先规范化为目标类型,再使用 ===

严格模式只收紧标量调用

文件顶部的 declare(strict_types=1); 启用 严格类型模式(strict types) 。在该文件发起的用户定义函数调用中,标量参数通常必须与声明精确匹配,唯一的放宽是 int 可以传给 float。严格规则由调用方文件决定,不由函数定义所在文件替调用方决定。

严格模式也会收紧声明所在文件中的返回类型处理,但它不是输入验证器。JSON、查询参数与表单值仍然以字符串、数组、整数或 null 等形式进入程序;代码必须决定允许哪些表示、范围与缺失状态。严格模式也不会检查 array 的元素形状。

示例

下面四个示例依次观察运行时类型、类型声明、输入解析与比较规则。所有输出都由本地 PHP 8.3.33 CLI 实际生成。

观察当前值的类型

同一个变量先绑定整数,再绑定内容相同的字符串。数组与生成器的具体运行时类型不同,但都满足 iterable

runtime_types.php
<?php

$value = 17;
printf("%s: %s\n", get_debug_type($value), $value);

$value = '17';
printf("%s: %s\n", get_debug_type($value), $value);

$items = ['draft', 'review'];
$stream = (function (): Generator {
    yield 'publish';
})();

foreach ([$items, $stream] as $collection) {
    printf(
        "%s iterable=%s\n",
        get_debug_type($collection),
        is_iterable($collection) ? 'yes' : 'no',
    );
}
int: 17
string: 17
array iterable=yes
Generator iterable=yes

第一、二行说明类型属于当前值,而不是变量名。字符串 '17' 看起来像数字,但在没有发生转换前仍是字符串。

iterable 描述可被 foreach 遍历的能力。检查得到的是 arrayGenerator,不会得到两个名为 iterable 的运行时值。

用联合类型与可空类型表达契约

订单编号允许数据库整数或外部文本编号,所以函数用联合类型接收两种形式。负责人确实可能不存在,因此另一个函数把 null 写进声明。

declared_types.php
<?php
declare(strict_types=1);

function normalizeOrderId(int|string $id): string
{
    return is_int($id)
        ? sprintf('ORD-%05d', $id)
        : strtoupper(trim($id));
}

function displayOwner(?string $owner): string
{
    return $owner ?? 'unassigned';
}

echo normalizeOrderId(27), PHP_EOL;
echo normalizeOrderId(' web-27 '), PHP_EOL;
echo displayOwner(null), PHP_EOL;

try {
    normalizeOrderId(2.5);
} catch (TypeError) {
    echo "TypeError\n";
}
ORD-00027
WEB-27
unassigned
TypeError

is_int() 将联合类型缩小到单一分支,此后两个分支都明确返回字符串。2.5 既不是 int 也不是 string,严格调用会在函数体执行前抛出 TypeError

联合类型应该表达真实的领域输入,不是为了避免设计边界而不断增加成员。若所有调用方都能先规范化为一种编号,接收单一值对象或字符串会更简单。

验证后再转换外部输入

查询参数常以字符串进入程序。这个解析函数接受 mixed,随后立即检查允许的输入类型、整数格式与非负范围。

parse_quantity.php
<?php
declare(strict_types=1);

function parseQuantity(mixed $raw): int
{
    if (is_int($raw)) {
        $quantity = $raw;
    } elseif (is_string($raw)) {
        $parsed = filter_var($raw, FILTER_VALIDATE_INT);
        if ($parsed === false) {
            throw new InvalidArgumentException('not an integer');
        }
        $quantity = $parsed;
    } else {
        throw new InvalidArgumentException('wrong input type');
    }

    if ($quantity < 0) {
        throw new InvalidArgumentException('negative quantity');
    }
    return $quantity;
}

foreach (['12', '0', '12 boxes', null] as $raw) {
    try {
        printf("%s => %d\n", get_debug_type($raw), parseQuantity($raw));
    } catch (InvalidArgumentException) {
        printf("%s => invalid\n", get_debug_type($raw));
    }
}
string => 12
string => 0
string => invalid
null => invalid

合法的字符串 '0' 被保留为整数 0,而带尾随文本的字符串与 null 被拒绝。直接执行 (int) ($raw ?? 0) 会把不同错误状态压成同一个整数,调用方就无法再区分它们。

mixed 在这里表示尚未建立信任的边界值,不表示后续代码可以随意对它做运算。完成分支检查后,$quantity 才具有业务代码可以依赖的 int 含义。

分开真值与相等性

条件位置检查真值,比较运算符检查两个操作数。两者都会遇到转换规则,但回答的是不同问题。

comparisons.php
<?php

$values = ['', '0', '00', [], [0]];

foreach ($values as $value) {
    printf(
        "%s => %s\n",
        json_encode($value),
        $value ? 'true' : 'false',
    );
}

var_export('10' == 10);
echo PHP_EOL;
var_export('10' === 10);
echo PHP_EOL;
"" => false
"0" => false
"00" => true
[] => false
[0] => true
true
false

字符串 '0' 是 PHP 中少见的非空假字符串,而 '00' 为真。用 if ($code) 判断业务代码是否存在,可能会误判合法的字符串 '0';应按契约检查 null、空字符串或格式。

最后两行的值相等但类型不同。输入已经规范化时使用 ===,可以避免比较语句再次引入隐式转换。

陷阱

把强制转换当作验证

**修复:**先限定允许的输入类型与文本格式,再转换并检查范围。错误状态需要返回结果对象还是抛出领域异常,应由接口契约明确决定。

认为 strict_types 会清洗请求数据

**修复:**在 HTTP、CLI、消息队列与数据库边界建立解析层。解析层输出窄类型后,内部函数的严格声明才有稳定含义。

依赖宽松比较与 empty()

**修复:**先写出允许的表示和缺失政策,然后规范化一次,并使用 ===is_*() 或针对领域的校验函数。不要用一条宽泛条件代替数据契约。

以为 array 声明包含元素类型

**修复:**在边界验证形状,并为稳定领域数据使用具名对象。PHPDoc 的数组形状与泛型可以帮助静态分析,但不能替代运行时校验。

把对象赋值当成对象复制

**修复:**需要新身份时显式 clone,并为内部可变对象定义克隆政策。若只需要共享对象,就用名称与接口清楚表达所有权,不要依赖读者猜测。

深入 运行时值与声明类型

运行时值与声明类型

运行时值类型和声明类型有重叠,但不能当作同一张枚举表。理解两者的差别,可以避免在反射、错误消息和静态分析之间来回套用错误名称。

原子声明与能力集合

boolintfloatstringarrayobjectnull 以及类或接口名都可以出现在合适的用户代码声明位置。resource 是例外:它是运行时类型,却不能作为用户代码类型声明。需要接收资源时,通常先接收 mixed,再用 is_resource()get_resource_type() 检查。

iterable 等价于接收 array 或实现 Traversable 的对象,因此值的具体类型仍是数组或某个类。callable 接受当前作用域中可调用的值,但原生语法无法约束它的参数与返回签名。可调用性还与作用域和可见性有关,所以外部字符串不应仅凭形状就当作可信回调。

callable 不能用作属性类型,因为属性保存期间不能保证该值在所有作用域一直可调用。需要把闭包存入属性时可以声明 Closure;需要更一般的回调时,应在构造边界验证并选择明确的内部表示。

联合、交集与 DNF

联合类型用 | 表示「至少满足一个成员」。PHP 会在编译时拒绝部分明显冗余的组合,例如 int|INTbool|falsemixed 已经包含所有值,因此也不能再与其他类型组成联合。

交集类型用 & 表示「同时满足所有成员」,成员必须是类或接口类型,不能写 int&Countable。它适合要求一个对象同时具有两项独立能力的 API,不适合表达数组元素约束。

PHP 8.2 起可以用析取范式类型组合交集与联合,例如 (Countable&Iterator)|array。交集部分必须带括号,类型表达式也应保持足够窄;如果签名已经难以口头说明,具名接口往往更容易维护。

mixedvoidnever

mixed 接受包括 null 在内的所有值。它适合尚未解析的边界或真正透明的传递层,但会把缩小类型的责任交给函数体。一个函数接收 mixed 后直接执行算术或成员访问,通常说明契约尚未完成。

void 只能用于返回类型,表示函数正常返回时不提供结果值。函数可以执行裸 return;,但不能返回一个表达式。调用 void 函数仍会得到 null,不过调用方不应把这个实现结果当作 API 数据。

never 也只能用于返回类型,表示函数不会正常返回,例如总是抛出异常或终止进程。声明为 never 的函数若运行到函数末尾或尝试正常返回,会违反声明。它表达控制流事实,不是一种可存入变量的值。

类型转换保留不了来源信息

转换规则只看当前值,不知道这个值来自可信配置还是用户请求。把字符串转成整数后,原始拼写、前导字符、缺失状态与被截断部分都可能消失,因此转换应该发生在已经决定接受该表示之后。

数字字符串与算术

格式正确的数字字符串可以参加算术,并按内容转成 intfloat。非数字字符串在 PHP 8 的算术上下文中会触发 TypeError;带尾随非数字内容的字符串也会产生诊断。依赖这种运算时转换,会让验证政策由表达式副作用决定。

明确的解析函数可以把语法错误、范围错误与缺失值分别处理。金额通常还需要十进制位数与舍入政策,不能先转成二进制浮点数再假设精确。数据类型只能提供载体,领域规则必须另外写出。

布尔转换不是文本解析

布尔强制转换不会把自然语言单词解释为真假。任何不属于假值集合的非空字符串都为真,所以 (bool) 'false'(bool) 'no' 都是 true。处理表单布尔值时,应枚举允许的文本或使用配置明确的验证函数。

同样,empty() 回答的是 PHP 的宽泛真值问题,不是「字段是否存在」或「文本是否为空白」。数组键存在性、可空值与空白字符串需要分别检查,否则合法零值很容易被当作缺失值。

比较前先规范化

宽松比较表随着操作数组合而变化,记住几个孤立示例不足以建立可靠契约。更稳妥的边界是把输入解析成一个确定类型,然后在内部使用严格比较。

如果接口确实允许多种表示,例如整数 10 和字符串 '10' 都表示同一编号,规范化函数应明确记录这个政策并返回一个类型。这样,转换只发生一次,后续授权、查找和分支不会各自采用不同规则。

严格模式的文件边界

strict_types 是文件级指令,而且参数规则看调用发生在哪个文件。一个定义在严格文件中的函数,若被非严格文件调用,标量参数仍可能在进入函数前被强制转换。反过来,严格文件调用定义在别处的用户函数时,会采用严格参数检查。

这项设计意味着库作者不能只靠自己文件里的声明,强迫所有调用方采用严格标量传参。库仍应声明准确类型,因为类型契约和 TypeError 很有价值;但真正的外部数据解析必须在库或应用的明确边界内完成。

内部函数调用还有自己的规则,不应从用户定义函数的调用位置规则外推。遇到边界行为时,用目标 PHP 版本运行一个最小测试,并以官方手册为准,不要让生成代码根据其他语言的严格模式猜测。

严格不等于静态

启用严格模式不会把 PHP 变成静态类型语言。变量仍可重新绑定不同类型,未声明位置仍可保存任意值,许多错误仍在执行到相应路径时才出现。

静态分析器可以利用原生声明、PHPDoc 与控制流提前发现更多问题,但它的结论取决于注解是否真实。把未经验证的 mixed 值用注释断言成窄类型,只会隐藏风险;运行时解析与静态契约需要描述同一个边界。

延伸阅读

检查点

5个问题 · 2 道输出预测题 · 1 道找错题

复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?