PHP 的複合類型(compound type)

我們時常熟練於使用某些東西,但事實上對它卻一無所知。

前言

根據 PHP 官方文件指出,PHP 的資料型態分為純類型(scalar types)、複合類型(compound types)及特殊類型(special types)

純類型包括: booleanintegerfloat/doublestring 
複合類型包括: arrayobjectcallableiterable 
特殊類型: resourceNULL

其中,複合類型有時是個令人困惑的存在,它可能會被任意改變,也可能一成不變。

實驗

$arr = ['a' => 1];
$obj = (object) ['a' => 1];
$callable = function () { return 1; };
function changeArray(array $a) {
$a['a'] = 2;
}
function changeObject(object $o) {
$o->a = 2;
}
function changeCallable(callable $c) {
$c = function () { return 2; };
}

我們定義三個 compound types: $arr$obj$callable
我們定義三個 functions,它會將傳入的值變更。

考量以下程式,試問執行結束後 $arr$obj$callable 的值分別為何

changeArray($arr);
changeObject($obj);
changeCallable($callable);

我們可以見到,僅有 $obj的值被改變了。

說明

淺拷貝(Shallow Copy)與深拷貝(Deep Copy)

在計算機科學中,如果要「複製」(Copy)有兩種常見方式:

  1. 申請一個記憶體位置,並儲存欲拷貝的值所在的記憶體位置。
    此複製方式稱為淺拷貝。
  2. 申請一個記憶體位置,將欲拷貝的值重新在這個記憶體位置中儲存。
    此複製方式稱為深拷貝。
在 Single Object下的 Shallow Copy 及 Deep Copy
在 Linked List 下的 Shallow Copy 及 Deep Copy

我們可以從上面兩張圖歸納出 Shallow Copy 及 Deep Copy 的幾項特性:

Shallow Copy 的空間複雜度為 O(1)時間複雜度為 O(1) 
Deep Copy 的空間複雜度為 O(n)時間複雜度為 O(n)

然而,在 Shallow Copy 時若改變 Original 的資料,會連帶影響到 Clone 出來的資料;Deep Copy 的資料則是 Original 與 Clone 互相獨立。

至此,我們可以推斷在 PHP 中, arraycallable在 function call 時是 Deep Copy,而 object則是 Shallow Copy。

設計理念?

在 Rust 中,變數的所有權(Ownership)有兩種與資料交互的管道:移動(Move)與複製(Clone)

對於 int這類具有固定大小的資料型態,預設使用 Clone 進行資料交互:

let x = 5;
let y = x;
// 在此處,x 及 y 都是可以合法使用的變數
println!("x = {}, y = {}", x, y);

對於 String這類可變大小的資料型態,預設使用 Move 進行資料交互:

let s1 = String::from("hello");
let s2 = s1;
// 在此處,s1 已不再是合法可以使用的變數,僅能使用 s2
// 因為 s1 的指標已被 Move 至 s2,而 Rust 預設是不允許無效的指標引用
println!("{}, world", s2);

回到 PHP,因為 object可能不是固定大小的資料型態(它可能是一個 Linked List 串接大量的 Node而組合而成的),故設計上採用 Shallow Copy

array因為它在被建立的時候就知道其記憶體大小(更新時也一併會更新該值),故設計上可以採用 Deep Copy。

如何繞過

在大部份情況下,預設設定是足夠使用的,然而寫程式最有趣的莫過於總會出現例外情況。

我們可以利用 & 運算子將原本是 Deep Copy 的轉為 Shallow Copy:

function changeArray(array &$arr) {
$arr['a'] = 2;
}
function changeCallable(callable &$callable) {
$callable = function () { return 2; };
}

同時也可以利用 clone 將原本是 Shallow Copy 的 object 轉為 Deep Copy

function changeObject(object $object) {
$newObj = clone $object;
$newObj->a = 2;
}

然而要注意的是,Deep Copy 會使 object 耗費過多資源(尤其是在 GC上),所以請謹慎使用。
話雖如此,大多數的 PHP 開發者並不會去在乎這些底層的開銷,而 PHP 的設計理念就是希望開發者不要太過於在意這些。

參考資料

  1. Understanding PHP’s internal array implementation
  2. PHP 7 Arrays: HashTables
  3. The Rust Programming Language: Secon Edition — Understanding Ownership
  4. [Javascript] 關於 JS 中的淺拷貝和深拷貝