【译】理解数组在PHP内部的实现(给PHP开发者的PHP源码-第四部分)
欢迎来到”给PHP开发者的PHP源码”系列的第四部分,这一部分我们会谈论PHP数组在内部是如何表示和在代码库里使用的。
为了防止你错过了之前的文章,以下是链接:
第一部分: 给PHP开发者的PHP源码-源码结构
第二部分: 理解PHP内部函数的定义
第三部分:PHP的变量实现
所有的东西都是哈希表
基本上,PHP里面的所有东西都是哈希表。不仅仅是在下面的PHP数组实现中,它们还用来存储对象属性,方法,函数,变量还有几乎所有东西。
因为哈希表对PHP来说太基础了,因此非常值得深入研究它是如何工作的。
那么,什么是哈希表?
记住,在C里面,数组是内存块,你可以通过下标访问这些内存块。因此,在C里面的数组只能使用整数且有序的键值(那就是说,你不能在键值0之后使用1332423442的键值)。C里面没有关联数组这种东西。
哈希表是这样的东西:它们使用哈希函数转换字符串键值为正常的整型键值。哈希后的结果可以被作为正常的C数组的键值(又名为内存块)。现在的问题是,哈希函数会有冲突,那就是说,多个字符串键值可能会生成一样的哈希值。例如,在PHP,超过64个元素的数组里,字符串”foo”和”oof”拥有一样的哈希值。
这个问题可以通过存储可能冲突的值到链表中,而不是直接将值存储到生成的下标里。
HashTable和Bucket
那么,现在哈希表的基本概念已经清晰了,让我们看看在PHP内部中实现的哈希表结构:
1 |
|
快速过一下:
nNumOfElements标识现在存储在数组里面的值的数量。这也是函数 count($array)返回的值。
nTableSize表示哈希表的容量。它通常是下一个大于等于 nNumOfElements的2的幂值。比如,如果数组存储了32元素,那么哈希表也是32大小的容量。但如果再多一个元素添加进来,也就是说,数组现在有33个元素,那么哈希表的容量就被调整为64。 这是为了保持哈希表在空间和时间上始终有效。很明显,如果哈希表太小,那么将会有很多的冲突,而且性能也会降低。另一方面,如果哈希表太大,那么浪费内存。2的幂值是一个很好的折中方案。
nTableMask是哈希表的容量减一。这个mask用来根据当前的表大小调整生成的哈希值。例如,”foo”真正的哈希值(使用DJBX33A哈希函数)是193491849。如果我们现在有64容量的哈希表,我们明显不能使用它作为数组的下标。取而代之的是通过应用哈希表的mask,然后只取哈希表的低位。
1 |
|
nNextFreeElement是下一个可以使用的数字键值,当你使用$array[] = xyz是被使用到。
pInternalPointer存储数组当前的位置。这个值在foreach遍历时可使用reset(),current(),key(),next(),prev()和end()函数访问。
pListHead和 pListTail标识了数组的第一个和最后一个元素的位置。记住:PHP的数组是有序集合。比如,[‘foo’ => ‘bar’, ‘bar’ => ‘foo’]和[‘bar’ => ‘foo’, ‘foo’ => ‘bar’]这两个数组包含了相同的元素,但却有不同的顺序。
arBuckets是我们经常谈论的“哈希表(internal C array)”。它用Bucket **来定义,因此它可以被看作数组的bucket指针(我们会马上谈论Bucket是什么)。
pDestructor是值的析构器。如果一个值从HT中移除,那么这个函数会被调用。常见的析构函数是zval_ptr_dtor。zval_ptr_dtor会减少zval的引用数量,而且,如果它遇到o,它会销毁和释放它。
最后的四个变量对我们来说不是那么重要。所以简单地说persistent标识哈希表可以在多个请求里存活,nApplyCount和bApplyProtection防止多次递归,inconsistent用来捕获在调试模式里哈希表的非法使用。
让我们继续第二个重要的结构:Bucket:
1 |
|
h是一个哈希值(没有应用mask值映射之前的值)。
arKey用来保存字符串键值。 nKeyLength是对应的长度。如果是数字键值,那么这两个变量都不会被使用。
pData及 pDataPtr被用来存储真正的值。对PHP数组来说,它的值是一个zval结构体(但它也在其他地方使用到)。不要纠结为什么有两个属性。它们两者的区别是谁负责释放值。
pListNext和 pListLast标识数组元素的下一个元素和上一个元素。如果PHP想顺序遍历数组它会从pListHead这个bucket开始(在HashTable结构里面),然后使用pListNext bucket作为遍历指针。在逆序也是一样,从pListTail指针开始,然后使用pListLast指针作为变量指针。(你可以在用户代码里调用end()然后调用prev()函数达到这个效果。)
pNext和 pLast生成我上面提到的“可能冲突的值链表”。arBucket数组存储第一个可能值的bucket。如果该bucket没有正确的键值,PHP会查找pNext指向的bucket。它会一直指向后面的bucket直到找到正确的bucket。pLast在逆序中也是一样的原理。
你可以看到,PHP的哈希表实现相当复杂。这是它使用超灵活的数组类型要付出的代价。
哈希表是怎么被使用的?
Zend Engine定义了大量的API函数供哈希表使用。低级的哈希表函数预览可以在 zend_hash.h文件里面找到。另外Zend Engine在 zend_API.h文件定义了稍微高级一些的API。
我们没有足够的时间去讲所有的函数,但是我们至少可以查看一些实例函数,看看它是如何工作的。我们将使用 array_fill_keys作为实例函数。
使用第二部分提到的技巧你可以很容易地找到函数在 ext/standard/array.c文件里面定义了。现在,让我们来快速查看这个函数。
跟大部分函数一样,函数的顶部有一堆变量的定义,然后调用 zend_parse_parameters函数:
1 |
|
很明显, az参数说明第一个参数类型是数组(即变量 keys),第二个参数是任意的zval(即变量 val)。
解析完参数后,返回数组就被初始化了:
1 |
|
这一行包含了array API里面存在的三步重要的部分:
1、Z_ARRVAL_P宏从zval里面提取值到哈希表。
2、zend_hash_num_elements提取哈希表元素的个数(nNumOfElements属性)。
3、array_init_size使用size变量初始化数组。
因此,这一行使用与键值数组一样大小来初始化数组到 return_value变量里。
这里的size只是一种优化方案。函数也可以只调用 array_init(return_value),这样随着越来越多的元素添加到数组里,PHP就会多次重置数组的大小。通过指定特定的大小,PHP会在一开始就分配正确的内存空间。
数组被初始化并返回后,函数用跟下面大致相同的代码结构,使用while循环变量keys数组:
1 |
|
这可以很容易地翻译成PHP代码:
1 |
|
跟下面的一样:
1 |
|
唯一不同的是,C的遍历并没有使用内部的数组指针,而使用它自己的pos变量来存储当前的位置。
在循环里面的代码分为两个分支:一个是给数字键值,另一个是其他键值。数字键值的分支只有下面的两行代码:
1 |
|
这看起来太直接了:首先值的引用增加了(添加值到哈希表意味着增加另一个指向它的引用),然后值被插入到哈希表中。 zend_hash_index_update宏的参数分别是,需要更新的哈希表 Z_ARRVAL_P(return_value),整型下标 Z_LVAL_PP(entry),值 &val,值的大小 sizeof(zval *)以及目标指针(这个我们不关注,因此是 NULL)。
非数字下标的分支就稍微复杂一点:
1 |
|
首先,使用 convert_to_string将键值转换为字符串(除非它已经是字符串了)。在这之前, entry被复制到新的 key变量。 key = **entry这一行实现。另外, zval_copy_ctor函数会被调用,不然复杂的结构(比如字符串或数组)不会被正确地复制。
上面的复制操作非常有必要,因为要保证类型转换不会改变原来的数组。如果没有copy操作,强制转换不仅仅修改局部的变量,而且也修改了在键值数组中的值(显然,这对用户来说非常意外)。
显然,循环结束之后,复制操作需要再次被移除, zval_dtor(&key)做的就是这个工作。 zval_ptr_dtor和 zval_dtor的不同是 zval_ptr_dtor只会在 refcount变量为0时销毁zval变量,而 zval_dtor会马上销毁它,而不是依赖 refcount的值。这就为什么你看到 zval_pte_dtor使用”normal”变量而 zval_dtor使用临时变量,这些临时变量不会在其他地方使用。而且, zval_ptr_dtor会在销毁之后释放zval的内容而 zval_dtor不会。因为我们没有 malloc()任何东西,因此我们也不需要 free(),因此在这方面, zval_dtor做了正确的选择。
现在来看看剩下的两行(重要的两行^^):
1 |
|
这跟数字键值分支完成后的操作非常相似。不同的是,现在调用的是 zend_symtable_update而不是 zend_hash_index_update,而传递的是键值字符串和它的长度。
符号表
“正常的”插入字符串键值到哈希表的函数是 zend_hash_update,但这里却使用了 zend_symtable_update。它们有什么不同呢?
符号表简单地说就是哈希表的特殊的类型,这种类型使用在数组里。它跟原始的哈希表不同的是他如何处理数字型的键值:在符号表里,”123”和123被看作是相同的。因此,如果你在$array[“123”]存储一个值,你可以在后面使用$array[123]获取它。
底层可以使用两种方式实现:要么使用”123”来保存123和”123”,要么使用123来保存这两种键值。显然PHP选择了后者(因为整型比字符串类型更快和占用更少的空间)。
如果你不小心使用”123”而不是强制转换为123后插入数据,你会发现符号表一些有趣的事情。一个利用数组到对象的强制转换如下:
1 |
|
对象属性总是使用字符串键值来保存,尽管它们是数字。因此 $obj->{123} = 'foo'这行代码实际上保存’foo’变量到”123”下标里。当使用数组强制转换的时候,这个值不会给改变。但当 $arr[123]和 $arr["123"]都想访问123下标的值(不是已有的”123”下标)时,都抛出了错误。因此,恭喜,你创建了一个隐藏的数组元素。
下一部分 下一部分会再次在ircmaxell的博客发表。下一篇会介绍对象和类在内部是如何工作的。

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

PHP和Python各有优势,选择依据项目需求。1.PHP适合web开发,尤其快速开发和维护网站。2.Python适用于数据科学、机器学习和人工智能,语法简洁,适合初学者。

PHP在电子商务、内容管理系统和API开发中广泛应用。1)电子商务:用于购物车功能和支付处理。2)内容管理系统:用于动态内容生成和用户管理。3)API开发:用于RESTfulAPI开发和API安全性。通过性能优化和最佳实践,PHP应用的效率和可维护性得以提升。

在PHP中,应使用password_hash和password_verify函数实现安全的密码哈希处理,不应使用MD5或SHA1。1)password_hash生成包含盐值的哈希,增强安全性。2)password_verify验证密码,通过比较哈希值确保安全。3)MD5和SHA1易受攻击且缺乏盐值,不适合现代密码安全。

PHP是一种广泛应用于服务器端的脚本语言,特别适合web开发。1.PHP可以嵌入HTML,处理HTTP请求和响应,支持多种数据库。2.PHP用于生成动态网页内容,处理表单数据,访问数据库等,具有强大的社区支持和开源资源。3.PHP是解释型语言,执行过程包括词法分析、语法分析、编译和执行。4.PHP可以与MySQL结合用于用户注册系统等高级应用。5.调试PHP时,可使用error_reporting()和var_dump()等函数。6.优化PHP代码可通过缓存机制、优化数据库查询和使用内置函数。7

HTTP请求方法包括GET、POST、PUT和DELETE,分别用于获取、提交、更新和删除资源。1.GET方法用于获取资源,适用于读取操作。2.POST方法用于提交数据,常用于创建新资源。3.PUT方法用于更新资源,适用于完整更新。4.DELETE方法用于删除资源,适用于删除操作。

在PHPOOP中,self::引用当前类,parent::引用父类,static::用于晚静态绑定。1.self::用于静态方法和常量调用,但不支持晚静态绑定。2.parent::用于子类调用父类方法,无法访问私有方法。3.static::支持晚静态绑定,适用于继承和多态,但可能影响代码可读性。

PHP通过$\_FILES变量处理文件上传,确保安全性的方法包括:1.检查上传错误,2.验证文件类型和大小,3.防止文件覆盖,4.移动文件到永久存储位置。

PHP类型提示提升代码质量和可读性。1)标量类型提示:自PHP7.0起,允许在函数参数中指定基本数据类型,如int、float等。2)返回类型提示:确保函数返回值类型的一致性。3)联合类型提示:自PHP8.0起,允许在函数参数或返回值中指定多个类型。4)可空类型提示:允许包含null值,处理可能返回空值的函数。
