PHP的SPL擴充套件庫(二)物件陣列與陣列迭代器

硬核專案經理發表於2021-10-12

在 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 的新增資料的方法,它預設是以數字下標的形式追加陣列內容的。

綜上所述,在最後的遍歷中,我們只列印出了 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<User> userList = new ArrayList()<User>;

//     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

相關文章