PHP的SPL扩展库(二)对象数组与数组迭代器

在 PHP 中,数组可以说是非常强大的一个数据结构类型。甚至我们可以把 PHP 中的数组说成是 PHP 的灵魂,而且这么说一点都不夸张。相比 Java 之类的静态语言来说,PHP 的数组没有长度限制,没有键值的类型限制,非常地灵活方便。数组是一种基本的结构类型,它和 Int 、String 这一类的类型是同一级别的,而今天我们要学习的,则是一种将对象当作数组来操作的概念。我们先学习它们的使用,最后再来说说这么做有什么用。
对象数组 对象数组对应的就是 ArrayObject 这个类。如果是想让自己的类变成这种对象数组那么直接继承这个 ArrayObject 就可以了。它的使用非常简单,它和数组的主要区别就是它是一个真实的对象,不是基本的数据结构。也就是说,对于 is_object() 和 is_array() 来说,它们的结果会有不同。而且,数组的操作都是通过外部的公共函数来实现的,而 ArrayObject 对象则有一些内部的方法,当然,你也可以继承它之后自己再扩展实现更多的方法。
直接从数组转换为对象数组
我们在实例化 ArrayObject 的时候,可以直接传递一个 数组 作为构造参数,那么这个对象数组的内容就是以这个传递进来的数组为基础的内容的。

$ao = new ArrayObject(['a' => 'one', 'b' => 'two', 'c' => 'three']); var_dump($ao); // object(ArrayObject)#1 (1) { //["storage":"ArrayObject":private]=> //array(3) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //} //} foreach ($ao as $k => $element) { echo $k, ': ', $element, PHP_EOL; } // a: one // b: two // c: three

对象数组实现了迭代器等相关接口,所以它是可以通过 foreach() 来进行遍历的。
实例化对象数组并赋值
除了直接传递一个构造参数外,我们还可以实例化一个空的对象数组,然后像操作普通数组一样操作它。
$ao = new ArrayObject(); $ao->a = 'one'; $ao['b'] = 'two'; $ao->append('three'); var_dump($ao); // object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(2) { //["b"]=> //string(3) "two" //[0]=> //string(5) "three" //} //}foreach ($ao as $k => $element) { echo $k, ': ', $element, PHP_EOL; // two three } // b: two // 0: three

我们可以使用数组下标的形式来操作这个对象,这是因为 ArrayObject 还实现了 ArrayAccess 接口,关于这个接口我们之前的文章也讲过 PHP怎么遍历对象?https://mp.weixin.qq.com/s/cFMI0PZk2Zi4_O0FlZhdNg。在这里有个需要注意的地方是,如果是以对象的属性方式来操作的话,这个属性是不属于可迭代内容的。从这一段和上面的测试代码中可以看出,数组内容是存放在一个 storage 属性中的,而我们直接操作的这个 a 属性则是和这个 storage 属性平级的。其实从这里我们就可以猜测出来,ArrayObject 在内部其实就是通过 ArrayAccess 接口的实现来操作这个 storage 中保存的数组内容的。另外,append() 方法是 ArrayObject 的添加数据的方法,它默认是以数字下标的形式追加数组内容的。
【PHP的SPL扩展库(二)对象数组与数组迭代器】综上所述,在最后的遍历中,我们只打印出了 b 和 0 这两个下标的内容。因为 a 是对象的属性,不在其所维护的数组 storage 中。
我们可以通过设置一个标记,其实也就是一个属性参数,来让这种属性赋值也成为和数组赋值一样的操作,也就是让上面的 a 属性这种形式的操作变成数组的赋值。
$ao->setFlags(ArrayObject::ARRAY_AS_PROPS); $ao->d = 'four'; var_dump($ao); // object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(3) { //["b"]=> //string(3) "two" //[0]=> //string(5) "three" //["d"]=> //string(4) "four" //} //}foreach ($ao as $k => $element) { echo $k, ': ', $element, PHP_EOL; // two three } // b: two // 0: three // d: four

通过 setFlags() 方法设置了 ArrayObject::ARRAY_AS_PROPS 之后,我们可以看到这个 d 属性操作直接进入到了 storage 中,也就是这种属性操作变成了数组操作。
偏移下标操作
和其它的数据结构一样,对象数组也是有一系列的游标偏移下标的操作的,其实也就是通过几个函数来操作下标数据。
var_dump($ao->offsetExists('b')); // bool(true) var_dump($ao->offsetGet('b')); // string(3) "two"$ao->offsetSet('b', 'new two'); var_dump($ao->offsetGet('b')); // string(7) "new two"$ao->offsetSet('e', 'five'); var_dump($ao->offsetGet('e')); // string(4) "five"$ao->offsetUnset('e'); var_dump($ao->offsetGet('e')); // NULL var_dump($ao->offsetExists('e')); // bool(false)

这里和其它的数据结构中的操作都类似,就不多做解释了。
排序
对于普通的数组来说,我们如果需要排序之类的操作的话,是需要使用普通数组相关的函数的,比如 sort() 或 ksort() 这些函数。而对象数组本身其实是一个对象,也就是说它是无法在这些普通数组函数中使用的。有兴趣的朋友可以用 sort() 、 array_map() 这些函数来试试能不能操作 ArrayObject 对象。所以,ArrayObject 对象中自带了一些数据操作函数,不过并不是很全面,也就是几个排序相关的操作而已。
$ao->asort(); var_dump($ao); // object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(3) { //["d"]=> //string(4) "four" //["b"]=> //string(7) "new two" //[0]=> //string(5) "three" //} //}$ao->ksort(); var_dump($ao); // object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(3) { //[0]=> //string(5) "three" //["b"]=> //string(7) "new two" //["d"]=> //string(4) "four" //} //}

当然,还有对应的 usort() 、uksort() 、natsort() 、natcasesort() 这几个排序的函数。
切换数组内容
对于对象数组来说,数据内容要么像数组一样赋值,要么在初始化的时候通过构造参数传递进来,其实还有一个方法函数,可以直接替换 ArrayObject 里面的所有数据内容。
$ao->exchangeArray(['a' => 'one', 'b' => 'two', 'c' => 'three', 'd' => 4, 0 => 'a']); var_dump($ao); // object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(5) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //["d"]=> //int(4) //[0]=> //string(1) "a" //} //}

其它属性功能
其它的属性功能还包括获得数据的数量、获得序列化的结果以及直接获取内部的数组数据等操作。
var_dump($ao->count()); // int(5) var_dump($ao->serialize()); // string(119) "x:i:2; a:5:{s:1:"a"; s:3:"one"; s:1:"b"; s:3:"two"; s:1:"c"; s:5:"three"; s:1:"d"; i:4; i:0; s:1:"a"; }; m:a:1:{s:1:"a"; s:3:"one"; }"var_dump($ao->getArrayCopy()); // array(5) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //["d"]=> //int(4) //[0]=> //string(1) "a" //}

另外,通过 ArrayObject 还可以直接获得相关数据的 ArrayIterator 迭代器对象。
var_dump($ao->getIterator()); // object(ArrayIterator)#1 (1) { //["storage":"ArrayIterator":private]=> //object(ArrayObject)#3 (2) { //["a"]=> //string(3) "one" //["storage":"ArrayObject":private]=> //array(5) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //["d"]=> //int(4) //[0]=> //string(1) "a" //} //} //} var_dump($ao->getIteratorClass()); // string(13) "ArrayIterator"

getIterator() 方法获得一个 ArrayIterator 对象,getIteratorClass() 方法则获得生成的 Iterator 对象类型。接下来我们就讲讲这个 ArrayIterator 数组迭代器。
数组迭代器 其实数组迭代器这个东西和 ArrayObject 对象数组其实没有什么太大的区别,甚至它们大部分的方法函数都是一样的。而唯一的不同就是 ArrayIterator 多了几个迭代器中的相关方法,另外,对于 ArrayIterator 来说,没有了 exchangeArray() 方法,因为它的本质是一个迭代器,而不是和 ArrayObject 一样是一个容器,所以如果完全切换了迭代器内部的内容,就相当于是变成了一个新的迭代器了。
$ai = new ArrayIterator(['a' => 'one', 'b' => 'two', 'c' => 'three', 'd' => 4, 0 => 'a']); var_dump($ai); $ai->rewind(); while($ai->valid()){ echo $ai->key(), ': ', $ai->current(), PHP_EOL; $ai->next(); } // a: one // b: two // c: three // d: 4 // 0: a// 游标定位 $ai->seek(1); while($ai->valid()){ echo $ai->key(), ': ', $ai->current(), PHP_EOL; $ai->next(); } // b: two // c: three // d: 4 // 0: a// foreach遍历 foreach($ai as $k=>$v){ echo $k, ': ', $v, PHP_EOL; } // a: one // b: two // c: three // d: 4 // 0: a

没错,它比 ArrayObject 就是多了 valid()、next()、key()、current()、rewind()、seek() 这几个迭代器相关的方法。
递归数组迭代器 除了普通的 ArrayIterator 之外,SPL 中还提供了可用于深度递归遍历的迭代器。我们来看看它和普通的这个 ArrayIterator 之间有什么区别。
$ai = new ArrayIterator(['a' => 'one', 'b' => 'two', 'c' => 'three', 'd' => 4, 0 => 'a', 'more'=>['e'=>'five', 'f'=>'six', 1=>7]]); var_dump($ai); // object(ArrayIterator)#1 (1) { //["storage":"ArrayIterator":private]=> //array(6) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //["d"]=> //int(4) //[0]=> //string(1) "a" //["more"]=> //array(3) { //["e"]=> //string(4) "five" //["f"]=> //string(3) "six" //[1]=> //int(7) //} //} //}$rai = new RecursiveArrayIterator($ai->getArrayCopy()); var_dump($rai); // object(RecursiveArrayIterator)#1 (1) { //["storage":"ArrayIterator":private]=> //array(6) { //["a"]=> //string(3) "one" //["b"]=> //string(3) "two" //["c"]=> //string(5) "three" //["d"]=> //int(4) //[0]=> //string(1) "a" //["more"]=> //array(3) { //["e"]=> //string(4) "five" //["f"]=> //string(3) "six" //[1]=> //int(7) //} //} //}while($rai->valid()){ echo $rai->key(), ': ', $rai->current() ; if($rai->hasChildren()){ echo ' has child ', PHP_EOL; foreach($rai->getChildren() as $k=>$v){ echo '',$k, ': ', $v, PHP_EOL; } }else{ echo ' No Children.', PHP_EOL; } $rai->next(); } // a: one No Children. // b: two No Children. // c: three No Children. // d: 4 No Children. // 0: a No Children. // more: Array has child //e: five //f: six //1: 7

这回的数据中,我们的 more 这个字段是一个多维数组。可以看到,不管是 ArrayIterator 还是 RecursiveArrayIterator ,它们打印出来的对象内容是没什么区别的,而区别又是在在于 RecursiveArrayIterator 提供的一些方法的不同。
RecursiveArrayIterator 这个递归数组迭代器中提供了 hasChildren() 和 getChildren() 这两个方法,用于判断及获取当前遍历的数据值是还有下级子数据内容。注意,这里通过 getChildren() 获取的子数组内容还是 RecursiveArrayIterator 对象哦。
如果在普通的 ArrayIterator 中,我们通过 is_array() 也可以完成这样的遍历操作,但是获得的数据内容只是普通的数组。这就是 RecursiveArrayIterator 和 ArrayIterator 的最主要区别。
集合类实例 最后我们来看看今天学习的这堆东西都有什么用。其实 ArrayObject、ArrayIterator、RecursiveArrayIterator 在表现形式上都差不多,都是一组可遍历的代替数组操作的对象。不过说实话,平常我们真用不上,毕竟 PHP 中的普通数组这个数据结构太强大了,而且提供的那些数组操作函数也非常好用,所以我们今天学习的内容估计很多同学根本就没有使用过。
而我在学习了这些内容后,马上就想到了一个场景。不知道有没有老 Java 开发程序员看到这篇文章,在很久以前我们写 Java 代码的时候,喜欢在实体 bean 中添加一个集合,用来保存当前这个 bean 的列表形式的数据,比如下面这样。
// class User { //public IList userList = new ArrayList(); //public function getUserList(){ //// 查询数据库 //// ..... //for(){ //$u = new User(); ////xxxxx //// 添加到集合中 //userList.add($u); //} //} // }

这样,我们在外部实例化这个 bean 之后,直接调用获取列表的方法,可以将数据保存在这个 userList 变量中了。现在还有没有这种写法我不知道,但当时确实是有过这么一种写法。如果要对应到 PHP 中的话,我们就可以使用 ArrayObject 这些功能类来实现。
class User extends ArrayObject{ public function getUserList(){ $this->append(new User()); $this->append(new User()); $this->append(new User()); } }$u = new User(); $u->getUserList(); var_dump($u); // object(User)#5 (1) { //["storage":"ArrayObject":private]=> //array(3) { //[0]=> //object(User)#4 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //} //[1]=> //object(User)#6 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //} //[2]=> //object(User)#7 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //} //} //}foreach($u as $v){ var_dump($v); } // object(User)#4 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //} //object(User)#6 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //} //object(User)#7 (1) { //["storage":"ArrayObject":private]=> //array(0) { //} //}

是不是有点意思,继承 ArrayObject 就可以了,然后连那个 userList 都不需要自己定义了,直接就可以遍历当前这个对象了。当然,具体业务具体分析,如果你的业务需求中有这样的要求,那么完全可以尝试一下哦。
总结 今天的内容说实话并不是非常常用的内容,但是在某些情况下确实可以为我们的业务开发带来一些新的思路。另外就是要理清楚 ArrayObject 和 数组,以及 ArrayObject 和 ArrayIterator 这些对象和数据结构之间的区别,这样在合适的情景下就可以选用合适的方式来实现我们需要的功能啦。
测试代码:
https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/01/source/4.PHP的SPL扩展库(二)对象数组与数组迭代器.php
参考文档:
https://www.php.net/manual/zh/class.arrayobject.php
https://www.php.net/manual/zh/class.arrayiterator.php
https://www.php.net/manual/zh/class.recursivearrayiterator.php

    推荐阅读