恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
php内核源码解析=类型系统——PHP的类型到底怎么运作的
首页
资讯中心
/
php内核源码解析=类型系统——PHP的类型到底怎么运作的
php内核源码解析=类型系统——PHP的类型到底怎么运作的
发布时间:2026/8/16 4:08:50
纯大白话还是老规矩。第八阶段75-84核心就一句话PHP 里类型到底是什么、它怎么管数据的。我先给你打个总地基再逐条拆。---先讲明白一个总概念zval 和类型标签PHP 里所有变量底层都是同一个结构体叫 zvalzend value。它里面有个类型标签一个数字比如-1整数long-6字符串string-7数组array-10对象object 关键zval 只是一个能装任何东西的盒子一个写着装的是什么类型的贴纸。PHP 说弱类型本质就是——同一个盒子今天装整数、明天装字符串、后天装对象都行只要把贴纸撕了换一张。 后面所有类型话题都在讲这张贴纸怎么换、怎么查、怎么卡。---75.弱类型全景——为什么PHP 这么随便PHP 的弱类型白话就是三个字看心情换。-自动装盒$a1;就往盒子里装个整数贴上整数贴纸。$a1;就装字符串贴字符串标签。-自动换盒你53PHP 一看——加号需要两个数字这个字符串是5于是运行时偷偷把字符串撕下标签、换成整数5再加8。你根本不用管。-查的是此刻的贴纸类型检查在运行期每刻查的是 zval 当时贴着的那张贴纸。 所以弱类型的底层真相不是没有类型是类型跟着盒子走、随用随换。它不算不管类型而是类型自动转换得特别勤快。---76.类型强制转换——拆盒重装的规则 当 PHP 需要把值转换成另一种类型要么你强制(int)要么它自己为了运算偷换走的就是三个核心转换函数 转字符串 zval_get_string-整数42→42-浮点3.14→3.14-布尔 true →1false →空串这是坑-null →-数组 →Array爆一个警告-对象 →调它__toString()没有就报错 转整数 zval_get_longlong就是整数-字符串42abc→42从头读到第一个非数字就停后面的 abc 扔掉-字符串abc123→0开头不是数字就是0-浮点3.9→3直接砍掉小数不是四舍五入向下截断-true →1false →0转浮点 zval_get_double-字符串3.14→3.14-整数2→2.0-规则和整数类似但能读小数点 重点坑-整数转浮点点保留精度问题-字符串转整数的读到数字就停和开头不是数字就是0这两个行为最容易踩到 一句话转换撕掉旧贴纸、按目标类型的规则重读内容关键是字符串转数字读到非数字就停、开头非数字就是0。---77.严格模式 strict_types ——就是关掉自动换标签你文件顶部写declare(strict_types1);本质是告诉 PHP别给我瞎换标签参数传错了就报错。 区别白话说-弱模式默认functionf(int$x){}你传f(3)→PHP 自动把3撕标签转成整数3→放行。-严格模式f(3)→PHP 一看你传的是字符串、它要整数——当场TypeError 报错绝不偷换。 底层strict_types 影响的是参数和返回值的转换检查。它的控制开关存在——注意关键点——调用方那个文件里不是定义方文件里。谁传来的值看谁的 开合规不合规。这就是为什么你可能遇到我明明声明了严格模式怎么还让我传字符串——因为调用是在另一个没开严格模式的文件里。 重要严格模式不禁止你显式(int)3——你明说转就转。它只禁止自动偷换。 一句话严格模式关掉参数/返回值的自动转换你传错类型就报错而且开关看的是谁来调用那个文件。---78.联合类型int|string ——一盒允许多种贴纸 functionf(int|string $x){}意思是$x 盒子里允许装整数或字符串。 底层编译类的参数表里类型不再是一个数字单类型而是一个类型列表一组允许的数字。运行时检查就是1.看 $x 此刻贴的是什么贴纸整数字符串。2.拿这个贴纸去允许列表里比对在列表里 →放行不在 →报 TypeError并且错误信息会把你给了 X但我只接受int|string说得清清楚楚。|就是或。灵活但要在前面注意可空类型其实就是联合类型的一种特例见81。 一句话联合类型给这个参数开一个允许哪些贴纸的清单运行时拿现在贴的贴纸去清单里查查到了就过。---79.交集类型 AB ——必须同时满足多个合同 交集类型和联合类型正好相反-unionA|BA 或 B满足一个就行。-intersection ABA 和 B必须同时是 A 又 B。 它几乎只用在类/接口上因为只有对象才能同时是多种东西。一个类 implements Foo,Bar它就同时是 Foo 又是 Bar。 所以 functionf(FooBar $x){}的意思是$x 必须是同时实现了 Foo 和 Bar 两个接口的那个对象。 底层检查看 $x 这个对象所属的类是否同时实现了列表里所有接口/继承所有类全都沾得上才放行缺一个就报错。 一句话交集类型必须同时满足全部条件因为只有对象能同时是好几样所以基本都用在接口/类上。---80.never/void返回类型——没结果的两种姿势 这俩都是告诉 PHP我没东西返回但语义完全相反-void函数正常跑完返回时什么都不给。可以return;不许return5;。允许函数最终走到结尾。调用它的代码当成没返回值拿来的都是 null。-never8.1新增函数根本不会返回——它要么抛异常要么调exit/die 直接结束程序。绝不允许它正常走到函数结尾。所以写 never 的函数代码最后一行哪怕正常结束了PHP 也报错。 底层差异-void是运行期编译期查有return值 就拦。-never 是调用关系分析PHP 分析你能不能走到结尾能走到哪怕理论上可能正常走完就报错——因为它约定了这函数要么炸、要么结束程序。 一句话void正常走完但不带东西never压根不许正常走完只能炸或退程序。---81.可空类型?int——就是联合类型的手写简写?int完全等于int|null。底层就是78的联合类型——允许列表是[整数贴纸,null贴纸]。 坑提醒-?int只对声明的那个参数/返回值可空不是变量的整个类型可空。-null 是独立的贴纸is_null。-8.0后建议直接写int|null?int是旧写法语义一样。 一句话?intint|null 的简写底层就是联合类型一个整数或空的允许清单。---82.类型声明的编译期检查 vs 运行期检查 这是最实用的一条分清楚哪些错写代码时就看得出、哪些跑到才炸 编译期PHP 读你代码阶段就报红-继承/实现接口时函数签名类型对不上比如父类f(int)子类f(string)参数类型变了。-返回值类型该是int你写 string签名层面写死的。-类里两个 trait 冲突方法之类的结构错误。 运行期得真跑起来才报-参数实际传入的值类型不对f(3)传给要整数的严格模式才报弱模式会先尝试转。-返回值运行时真算出来的值类型不对。-declare(strict_types)归属、动态属性8.2检查、魔术方法__get 等的类型。 一句话概括规律凡是静态写死在代码签名上的、能一眼看出来的继承签名、返回值标记→编译期凡是跟运行时的实际值有关的 →运行期。签名是纸面声明值是实际交付纸面错了编译就抓交付错了跑起来才抓。---83.协变返回/逆变参数——PH8.0的更具体/更宽泛这个词最吓人其实就两个规则 返回类型协变covariant子类重写父类方法时返回类型可以更具体更小、更严格。-父functionmake():Animal-子functionmake():DogDog 是 Animal 的子类✅ 合法-意思子类答应我记得更具体这是安全的因为 Dog 也算 Animal。 参数类型逆变contravariant子类重写方法时参数类型可以更宽泛更大、更宽松。-父functionfeed(Dog $d)-子functionfeed(Animal $a)✅ 合法-意思子类答应的我能喂更宽的范围调用方传它的参数肯定能满足。 底层实现重写方法时 PHP 会做一次类型包含关系检查-返回类型子类的方法返回值类型必须是被父类返回类型包含在内子类更窄 ⊂父类更宽→协变-参数类型子类的参数类型必须包含父类的参数类型子类更宽 ⊃父类更窄→逆变 记口诀返回往窄走子类化参数往宽走父类化。方向不同别搞反。 一句话重写方法时返回类型你只能更具体、参数类型你只能更宽泛——PH会算包含关系来校验方向弄反就报错。---84.自己给 PHP 加一个新基本类型实战 这是终极挑战我给一条完整可落地的路线大白话关键实现点不创建文件 目标加一个 sayhi 类型的简单基本类型——一个直接存C 字符串的基本值类型演示最小闭环。 第1步给类型枚举加一个新的数字标签 在 zend_types.h 里找类型常量区加一个#defineMYZVAL_TYPE_SAYHI(IS_PTR1)// 或挑一个空闲的类型号类型号是 zval 贴纸的编号新类型必须占一个没被占用的号。 第2步定义这个类型的值结构体新基本类型要能存东西。定义一个 C 结构typedefstruct{char*buf;// 具体存的内容size_tlen;}my_zval_sayhi;第3步在 zval 里想办法让它能装这个值 zval 有 value 联合体。最省事的做法是不改 zval 核心而是存一个指针到 zval 的 ptr 字段zval 里本来有个 ptr类型贴成 SAYHI运行时看到 SAYHI 就把 ptr 当作 my_zval_sayhi*。 第4步实现转换三件套这是核心 新类型要让(string)$x、(int)$x、(bool)$x 都能用就得在转换switch里补分支-get_zval_value_ptr/Z_TYPE 等各种宏-zend_get_string_value 里加caseSAYHI →从 ptr 读出 buf 转成 zstring-zend_get_long_value/zend_get_double_value 里加caseSAYHI →转数字 这几个函数在 zend_operators.c漏了任何一个用户在类型之间随便转就会崩。 第5步实现比较分支、走到比较函数zend_compare加 SAYHI 的比对逻辑两个都是 SAYHI 就比 bufSAYHI 和其他类型就按规定转。 第6步实现内存管理最容易漏-创建给 my_zval_sayhi 分配内存、复制字符串。-释放zval_ptr_dtor 里加 SAYHIcase负责 free 掉 buf——不忘就内存泄漏。-COPYzend_copy_ctor 加case深复制 buf。 第7步把它暴露成像语法类型一样可用-加对应扩展zend_register_class_specialties 或在启动时 zend_declare_function 注册一个sayhi($s)返回一个 SAYHI 类型 zval 的函数。-这样用户 $xsayhi(abc);就拿到新类型。 第8步重新编译 PHP测试 $ xsayhi(a);var_dump($x);→期望输出你的类型和内容。跑(string)$x、(int)$x、、serialize序列化也要给 SAYHI 加规则否则序列化直接崩。 完整流程顺序别乱类型号 →值结构 →zval 存取 →转换三件套 →比较 →内存管理(alloc/free/copy)→暴露语法/函数 → 编译全场景测试转换/比较/序列化/内存泄漏检测。 坑总结必看大多数人加新类型死在三处——忘了在dtor(free)里释放 →内存泄漏②忘了在转换switch里补分支 →一(int)就崩③忘了 serialize/unserialize →一序列化就崩。把这三处补全新类型基本就成了。 一句话加基本类型占个类型号 →定义值结构 →放进 zval →补转换/比较/内存/序列化四个switch分支 →暴露给 PHP 用 → 重新编译 →全场景测试核心是别漏内存释放和类型转换。---75-84总账 地基所有值都是 zval万能盒子一块类型贴纸弱类型随用随换贴纸75弱类型运行时看贴纸、随用随换不是没类型是换得勤76转换撕贴纸重读内容字符串→数字读到非数字就停、开头非数字077严格模式关掉参数/返回值自动偷换传错就报 TypeError开关看调用方文件78int|string给参数开一张允许哪些贴纸清单运行时比对贴纸79AB必须同时满足多半用在接口/类上只有对象能同时是好几样80void能正常走完但不带东西never不许走完只能炸或退出程序81?intint|null 简写就是联合类型82签名写死的继承签名/返回标记→编译期抓运行时的实际值 →运行期抓83重写方法返回往窄走协变、参数往宽走逆变PHP 算包含关系校验84加新类型类型号→值结构→zval存取→转换/比较/内存/序列化四个switch→暴露→重编译→全测---有个点特别提醒你76的字符串转整数 0 和截断、以及77严格模式看调用方文件、还有82的编译期/运行期分界线这三个是 实际开发踩坑率最高的。前两个遇到过就记住第三个能用来看代码报错时判断是我写错了签名还是是运行时的数据不干净。