SPL 陣列過載

fangle發表於2017-05-27

前言

陣列過載: 陣列過載是指將物件作為陣列使用的過程。具有這種功能的物件也稱為索引器。

陣列過載是將物件作為陣列使用的一個過程。這意味著允許使用 [ ] 陣列語法訪問資料。

陣列過載的學習令我想起一個問題: 為何JavaScript中的一切皆物件?或者簡單的說是為何JavaScript的陣列是物件,可以以arr.lenth的方式調屬性,以arr.append()的方式調方法呢?

當然對於大多的JavaScript Developer,是不會有這個疑問的。因為JavaScript中沒有陣列這種變數型別。而大多數第一門語言是C或者是PHP的開發者,第一次接觸JavaScript的陣列時,不免會覺得彆扭。

如果你也是一個無法理解JavaScript的陣列是物件的PHPer,覺得很不可思議,這篇文章或許能幫助你。好了,先把問題放下。我們來學習SPL的陣列過載,說不定學著學著就明白了呢。


一、ArrayAccess Interface

ArrayAccess Interface:PHP提供的索引器介面,屬於預定義介面之一,是陣列過載的核心,它提供了掛載到Zend引擎所必須的功能。

索引器介面:該介面提供了將物件作為陣列訪問的功能,也稱為陣列式訪問介面或陣列過載介面。

ArrayAccess Interface
實現ArrayAccess介面的類,就是一個索引器。可以使用標準的陣列語法讀取和操縱物件中的內容。 注意:ArrayAccess介面提供訪問陣列一樣訪問物件能力的介面,但不提供迭代訪問能力。所以不能迭代訪問該索引器,即不能foreach。除非該索引器同時實現了迭代器介面。同理不能對該索引器使用count計數函式,除非其實現計數器介面

ArrayAccess Interface 實現:

<?php
//自定義索引器類 實現 ArrayAccess介面
class MyArray implements ArrayAccess{
	protected $_arr =array() ;	//用於存放資料
	
	//設定或替換給定偏移量上的資料
	public function offsetSet($offset,$value){
		$this->_arr[$offset] = $value;
	}
	//返回指定偏移量位置上的資料
	public function offsetGet($offset){
		return $this->_arr[$offset];
	}
	//判斷指定的偏移量是否存在於陣列中
	public  function offsetExists($offset){
		return array_key_exists($offset,$this->_arr);
	}
	//刪除指定偏移量位置上的資料
	public function OffsetUnset($offset){
		unset($this->_arr[$offset]);
	}
}

//使用示例
$myArr = new MyArray;
//賦值
$myArr['first']=1;
$myArr['second']=2;
//使用$myArr['second']
echo $myArr['second'].PHP_EOL;
//改變$myArr['second']值
$myArr['second']='two';
//再次使用$myArr['second']
echo $myArr['second'].PHP_EOL;
//刪除$myArr['second']
unset($myArr['second']);
//再次使用$myArr['second']
echo $myArr['second'].PHP_EOL;

複製程式碼

執行結果:

SPL 陣列過載
這裡我們可以看到實現ArrayAccess介面後,我們可以把一個物件當做陣列一樣賦值使用刪除某個鍵值。 為什麼實現該介面,就有這種功能呢?我們來一探究竟。與往常的套路一樣我們在每個方法裡,加上怎麼一句

echo __METHOD__,PHP_EOL;

<?php
//自定義索引器類 實現 ArrayAccess介面
class MyArray implements ArrayAccess{
	protected $_arr =array() ;	//用於存放資料
	
	//設定或替換給定偏移量上的資料
	public function offsetSet($offset,$value){
		echo __METHOD__,PHP_EOL;
		$this->_arr[$offset] = $value;
	}
	//返回指定偏移量位置上的資料
	public function offsetGet($offset){
		echo __METHOD__,PHP_EOL;
		return $this->_arr[$offset];
	}
	//判斷指定的偏移量是否存在於陣列中
	public  function offsetExists($offset){
		echo __METHOD__,PHP_EOL;
		return array_key_exists($offset,$this->_arr);
	}
	//刪除指定偏移量位置上的資料
	public function OffsetUnset($offset){
		echo __METHOD__,PHP_EOL;
		unset($this->_arr[$offset]);
	}
}

//使用示例
$myArr = new MyArray;
//賦值
$myArr['first']=1;
$myArr['second']=2;
//使用$myArr['second']
echo $myArr['second'].PHP_EOL;
//改變$myArr['second']值
$myArr['second']='two';
//再次使用$myArr['second']
echo $myArr['second'].PHP_EOL;
//刪除$myArr['second']
unset($myArr['second']);
//再次使用$myArr['second']
echo $myArr['second'].PHP_EOL;
複製程式碼

再次執行:

SPL 陣列過載
通過分析,我們可以知道:當我們實現ArrayAccess介面後,我們以陣列的方式給索引器物件賦值時,PHP自動幫我們呼叫了offsetSet方法。echo 這個索引器物件某個鍵值時,則呼叫了offsetGet方法。isset判斷該鍵值時,則自動呼叫offsetExists方法。刪除該鍵值時,則自動呼叫offsetUnset方法。

通過上面的程式碼,我們可以看到實現ArrayAccess介面的一個簡單形式。它為我們演示了陣列機制是如何運作的

提供ArrayAccess介面的主要原因是並非所有的集合都是基於真實的陣列。使用ArrayAccess介面的可能會將請求代理到面向服務的架構(SOA)的後端程式,或者其他形式的離執行緒序。這允許推遲底層陣列的獲取時間,直到它被實際訪問時才去獲取這些資料。

然而,對於大多數情況來說,可能會使用陣列作為底層資料的表達形式。然後,你將會向這個類新增處理這一資料的方法。為實現這一目的,SPL提供了內建的ArrayObject類。


二、Countable Interface

Countable Interface:SPL提供的計數介面,實現該介面的類可被用於**count()**函式計數,並返回資料長度。

Countable Interface

我們知道,當一個類實現了ArrayAccess介面,就可以把它當成一個陣列一樣操作。但是並不能使用count函式進行計數。如果我們想讓這個物件更像一個陣列,就必須實現計數介面,為它提供計數能力。

實現Countable介面非常簡單,只需要實現它的抽象方法count方法。由手冊我們知道,count方法必須返回一個int型別的值,實際上這個值就是Array物件的有效元素數目。

程式碼示例:

//自定義陣列物件實現Countable介面
class MyArray implements Countable{
	protected $_arr = array(1,2,3); //為演示方便直接賦值
	public function count(){
		return count($this->_arr);
	}
}
//例項化自定義陣列物件
$myArr = new MyArray;
//計數
echo count($myArr);
複製程式碼

執行結果:

SPL 陣列過載

同樣的,我們加上一句 echo __METHOD__,PHP_EOL;

<?php
//自定義陣列物件實現Countable介面
class MyArray implements Countable{
	protected $_arr = array(1,2,3); //為演示方便直接賦值
	public function count(){
		echo __METHOD__,PHP_EOL;
		return count($this->_arr);
	}
}
//例項化自定義陣列物件
$myArr = new MyArray;
//計數
echo count($myArr);
複製程式碼

再次執行:

SPL 陣列過載
如我們意料中的一樣,實現Countable的介面後,當我們把這個物件傳遞給Count函式時,PHP會幫我們自動呼叫物件裡的Count方法。

當然,這個有一個問題: 為什麼SPL內建的一個實現Countable介面的類ArrayObject,實現count方法時,方法體裡無實現程式碼,也沒有返回值,但是依然能夠計數。而我們如果沒有return,則預設是null。使用Count函式計數時,就會返回零。為什麼ArrayObject不用返回呢?

SPL 陣列過載
這個很納悶,如果你知道答案,請你告訴我。謝謝! 具體怎麼回事,這裡給出ArrayObject原始碼地址

好了,雖然存在一點疑問,但是不影響。Countable介面的實現就是怎麼簡單。

如果說我們有一個類同時實現了ArrayAccessCountable,甚至還有Iterator介面,那麼這個類的操作幾乎和陣列無差異了。實現ArrayAccess介面擁有陣列訪問,實現Countable介面擁有計數的能力,實現Iterator介面擁有迭代訪問的能力。


三、ArrayObject class

ArrayObject : ArrayObject是SPL內建的一個陣列物件類,它同時實現了ArrayAccessCountableIteratorAggregate介面。提供了迭代,排序和處理資料非常有用的方法,與陣列的使用幾乎是無差別的。其中實現IteratorAggregate介面委託的迭代器是在構造方法傳遞引數中預設了ArrayIterator迭代器。

SPL 陣列過載

ArrayObject簡單使用:

$myArr = new ArrayObject ;
//賦值
$myArr['first']=1;
$myArr['second']=7;
$myArr['third']=5;
$myArr['fourth']=9;
//列印
print_r($myArr);
//刪除
unset($myArr['fourth']);
//遍歷
foreach($myArr as $key => $val){
	echo $key.'=>'.$val.PHP_EOL;
}

複製程式碼

執行結果:

SPL 陣列過載

ArrayObject內部是基於Array實現的,所以它的限制在於只能處理真實的已經完全填充好的資料。但是它可以為應用程式提供有價值的基類。

也就是說,如果你要自定義一個實現ArrayAccess介面的類,實際上很多時候我們直接繼承ArrayObject就可以了。

ArrayObject的好處在於,你自定義的類可以繼承它可以得到與陣列使用無差異的功能。

好了,到這裡。你想想例項化一個SPL的ArrayObject物件,得到了什麼?

一個與陣列使用無差異的物件

記住上面這句話,它意味著什麼呢?我們再想一下為什麼JavaScript中陣列也是物件呢?SPL提供的ArrayObject不就和JavaScript的陣列很相似了嗎?打破思維的藩籬,我們換一個視角來看這個問題。

假如PHP一開始就和JavaScript一樣,沒有提供陣列這種變數型別,只給我們提供了ArrayObject類。甚至array()函式返回的不是一個陣列,而是一個ArrayObject的例項。我們不是也可以通過ArrayObject創造出一個與傳統陣列概念上使用無差異的**‘新陣列’嗎。而這種新陣列的使用與傳統陣列無差別,且這種新陣列還能呼叫方法,呼叫屬性。這不正是JavaScript中陣列嗎?那麼這個時候,這種概念的陣列,不就是所謂的陣列也是物件**嗎?

所以說,JavaScript的陣列從這個角度上看,就和PHP中SPL提供的ArrayObject的例項化陣列物件一樣。JavaScript沒有像PHP這種純粹的陣列,它定義的陣列就已經是一個物件了。是的,PHP的陣列相對於JavaScript是底層了,PHP完全可以封裝出JavaScript那種陣列物件。

當然,相較於JavaScript,只有PHP的陣列不是物件,它就算是純粹的陣列嗎?未必吧。PHP的陣列與C語言比較呢?C語言的陣列是無法動態擴充套件的,而PHP是可以的。從這個角度上來看,PHP中的陣列是否相較與C語言也是一種封裝呢?


四、使用ArrayObject內建方法進行排序

實現ArrayAccess介面的索引器,包括ArrayObject的例項雖與陣列使用幾乎無差異,但卻無法使用PHP提供的sortasort,ksort等函式進行排序。

$myArr = new ArrayObject ;
//賦值
$myArr['first']=1;
$myArr['second']=7;
$myArr['third']=5;
$myArr['fourth']=9;
//列印
asort($myArr);
複製程式碼

執行結果:

SPL 陣列過載

但是如果你這樣寫:

$arr = new ArrayObject;
$arr = [1,2,4,5,8,3];
print_r($arr);
sort($arr);     //排序
print_r($arr);
複製程式碼

執行結果:

SPL 陣列過載
貌似是可以使用的,但是實際上是不行的。 因為你寫$arr = [1,2,4,5,8,3];時,已經為$arr重新賦值一個陣列了,$arr的變數型別已經不是ArrayObject物件,而是陣列型別了。陣列當然能呼叫asort函式咯。

所以,實現ArrayAccess介面的陣列物件無法使用PHP自帶的排序函式進行排序,這點你要注意。 不過,方便的是SPL中ArrayObject類已經擁有asort,ksort等方法它讓我們可以實現類似於JavaScript那種操作,呼叫$arr->asort();給這個陣列物件排序。

程式碼示例:

$myArr = new ArrayObject ;
//賦值
$myArr['first']=1;
$myArr['second']=7;
$myArr['third']=5;
$myArr['fourth']=9;
//排序
$myArr->asort();
//列印
print_r($myArr);
複製程式碼

執行結果:

SPL 陣列過載


五、SPL購物車

ArrayObject的最常見應用是在web購物車中,令購物車類繼承自ArrayObject,這樣你例項化的購物車,不僅是一個物件而且還是一個陣列。

程式碼示例:

<?php
//商品類
class Product{
	public $_name;	//商品名稱
	public $_price;	//商品價格
	public function __construct($name,$price){
		$this->_name = $name;
		$this->_price = $price;
	}
}
//購物車類
class Cart extends ArrayObject{
	//計算購物車商品總價格
	public function sum(){
		$num = 0;
		foreach($this as $product){
			$num+=$product->_price ;
		}
		return PHP_EOL.'購物車總價 : '.$num.PHP_EOL;
	}
}

//例項化商品
$book = new Product('<補刀心法>',57);
$pen = new Product('破鐵牌鋼筆',2);
//例項化購物車
$cart = new Cart;
$cart[] = $book;
$cart[] = $pen;
//檢視列印
print_r($cart);
//檢視購物車商品總數
echo count($cart);
//計算購物車商品總價格
echo $cart->sum();
複製程式碼

執行結果:

SPL 陣列過載
從上面程式碼可以看到,通過讓購物車類繼承ArrayObject,我們可以以陣列的形式新增商品,計算商品個數,購物車總金額。使用這種方法可以讓購物車物件的操作變得更加簡單方便。

當然這裡我們要強調一點,storage屬性是ArrayObject的私有屬性,Cart作為ArrayObject的子類,是無法直接訪問和使用的。但是由於ArrayObject的特性,我們可以在Cart中使用$this來訪問父類的storage屬性,同理可以使用$this[0]來訪問第一個元素。

ArrayObject 實現了ArrayAccess介面,所有繼承它的類例項出來的物件都可以當做陣列一般使用。其實,ArrayObject的構造方法還允許傳入一個陣列,它可以讓我們把一個陣列當成物件一般使用。

$arr = [1,8,3,9,5];
$arr = new ArrayObject($arr);
$arr->append(2);  //新增一個元素
echo $arr->offsetGet($arr->count()-1);   //獲取最後一個資料 
複製程式碼

執行結果:

SPL 陣列過載


六、使用物件作為陣列鍵值

有些時候,我們或許需要將物件的作為陣列鍵值。在PHP中如果直接這麼做,明顯是不行的,會得到一個警告。

<?php
class MyObject{}
$a = new MyObject;
$arr = array($a=>'test');
複製程式碼

執行結果:

SPL 陣列過載

幸運地是,SPL為我們提供一個獲取物件雜湊值的函式spl_object_hash()

SPL 陣列過載
這個函式可以為所有的物件例項建立一個唯一的識別符號。即使兩個物件屬於同一類,它的識別符號依然是唯一的。

<?php
class MyObject{}
$a = new MyObject;
$b = new MyObject;

echo spl_object_hash($a);
echo PHP_EOL;
echo spl_object_hash($b);
複製程式碼

SPL 陣列過載
於是我們可以通過這個函式間接實現把一個物件當作陣列的鍵值

<?php
class MyObject{}
$a = new MyObject;
//使用物件作為陣列鍵值
$arr = array(spl_object_hash($a)=>'test');
//輸出
echo $arr[spl_object_hash($a)];
複製程式碼

執行結果:

SPL 陣列過載

使用物件作為鍵值的好處是可以在一個陣列中避免存放同個物件的多個引用。

<?php
class MyObject{}
$a = new MyObject;
$b = $a;

//賦值
$arr[spl_object_hash($a)] = $a;    
$arr[spl_object_hash($b)] = $b;
//列印
print_r($arr);
複製程式碼

執行結果:

SPL 陣列過載
由於$a$b是同一個物件例項,spl_object_hash()返回值相同,所以key值相同。$arr[spl_object_hash($b)] = $b;並沒有為陣列建立新元素。保證了陣列中相同物件例項只有一個。

這裡我們還要強調一點:雖然在一個指令碼中對一個物件獲取多次雜湊值,得到的結果是一樣的。但是每次執行這個指令碼獲取這個物件的雜湊值是不同的。

<?php
class MyObject{}
$a = new MyObject;
$b = $a;


echo spl_object_hash($a);    //第一次獲取
echo PHP_EOL;
echo spl_object_hash($b);   //第二次獲取
複製程式碼

執行結果:

SPL 陣列過載


上一篇:SPL迭代器介面介紹

感謝閱讀,由於筆者能力有限,文章不可避免地有失偏頗 後續更新SPL的其他內容,歡迎大家評論指正


我最近的學習總結:


歡迎大家關注我的微信公眾號 火風鼎

相關文章