PHP 的引用傳遞與多階層分類

在 PHP 中,我們可以使用等號 ( = ) 將一個值賦予給一個新的變數(應該很多程式語言都是這樣)。

$a = 'Hello';

$b = $a;

這時候 $b 會新增一個記憶體位址來存放值,如果對 $b 進行修改,並不會影響到 $a 的值。

$a = 'foo';

$b = $a;

$a = 'bar';

// 'bar'
echo $a;

// 'foo'
echo $b

在 PHP 中,我們可以使用 & ,將不同的變數指向同一個內容。

例如下方使用 &$a 的記憶體位址賦值給 $b。這時候,如果對 $b 進行修改,$a 的值也會跟著一起變更。

$a = 'foo';

$b = &$a;

$a = 'bar';

// 'bar'
echo $a;

// 'bar'
echo $b

在 PHP 官方文件中介紹引用的部分,底下有一位大大的留言,對於引用的解釋非常到位。

It just likes a person who has two different names. 
就像一個人 (值) 有兩個不一樣的名字 (變數名稱)。

函式中參數的引用


引用還可以使用在函式的參數中,呼叫函式並傳入變數時,就會傳入變數的引用給函式內的參數。
在函式內對參數所做的任何更動,都會影響到函式外部的變數。

function foo(&$var)
{
    $var++;
}

$a = 5;

foo($a);

// $a is 6 here
echo $a;

只有兩種內容可以被引用。

  • 變數
  • 可以返回引用的函式,如 function &foo()

一般的常數如數字與字串是不能被引用的。

// error : const cannot be passed by reference
foo(1);

從下方的例子來看什麼是「可以返回引用的函式」。
如果 function &bar() 中沒有加上 &,代表函式返回就只是單純的值,那麼在 foo(bar) 就會發生錯誤,因為 foo() 的參數必須是可以被引用的內容。

function foo(&$var)
{
    $var++;
}

function &bar()
{
    $a = 5;
    return $a;
}

// 如果 function &bar() 沒有加上 &,這裡就會拋出錯誤
foo(bar());

「可以返回引用的函式」並不是代表一定會返回引用,而是根據是否使用 & 來判斷要不要返回引用。

function &test()
{
    static $b = 0; // 宣告一個靜態變數

    $b++;
    echo $b;

    return $b;
}

// $a 接收的是一般的返回值 $b
$a = test(); // 1
$a = 5;
$a = test(); // 2

// $a 接收的是返回值的引用,所以會與 $b 綁在一起
$a = &test(); // 3
$a = 5;
$a = test(); // 6

物件的引用


其實在 PHP 中,物件賦值給一個新的變數時,是透過引用的方式來賦值。
因此對新變數的任何變更,也會修改到原本的物件。

class a
{
    public string $abc = "ABC";
}

$b = new a();
$c = $b;

echo $b->abc; // ABC
echo $c->abc; // ABC

$c->abc = "DEF";
echo $b->abc; // DEF

取消引用


當 unset 一個引用,只是斷開了變數名和變數內容之間的綁定。 這並不意味著變數內容被銷毀了。

$a = 1;
$b = &$a;

unset($a);

官方文件這裡用 unix 的 unlink 指令來類比。

使用引用將一維陣列轉換為多維陣列


舉一個日常生活中常見的例子,電商網站裡種類繁多的商品分類。

電商網站商品分類
│
├── 3C 產品
├── 奶粉
│   └── 進口奶粉
│       └── 澳洲進口奶粉
└── 水果
    └── 蘋果
        ├── 紅蘋果
        └── 青蘋果

通常在資料庫中儲存多階層的商品分類,我們會使用 parent_id 欄位,在 parent_id 中儲存父  id,以此知道該分類被分在哪個分類底下。

id	name	    parent_id
1	水果	    0
2	奶粉	    0
3	蘋果	    1
4	青蘋果	    3
5	紅蘋果	    3
6	進口奶粉	    2
7	澳洲進口奶粉	6
8	3C 產品	    0

因為資料表中只能儲存一維的資料,將資料撈取出來之後還需要將其整理為多維陣列,通常會使用遞迴來處理,但另外一種處理方式就是使用引用傳遞 。

例如我們可以在迴圈中使用引用傳遞,這樣在迴圈中對陣列元素的所有操作,都會影響到原本的陣列。

<?php

$array = [
    'hello',
    'world',
];

foreach ($array as &$item) {
    if ($item === 'world') {
        $item = 'foobar';
    }
}

var_dump($array);
// array(2) {
//   [0]=>
//   string(5) "hello"
//   [1]=>
//   &string(6) "foobar"
// }

利用這個概念,我們就可以使用引用傳遞來將一維陣列轉換為多維陣列

<?php

// 陣列的 key 值需要與 id 相等,方便比對
// parentId === 0 為第一層
$array = [
    1 => ['id' => 1, 'parentId' => 0, 'name' => '水果'],
    2 => ['id' => 2, 'parentId' => 0, 'name' => '奶粉'],
    3 => ['id' => 3, 'parentId' => 1, 'name' => '蘋果'],
    4 => ['id' => 4, 'parentId' => 3, 'name' => '青蘋果'],
    5 => ['id' => 5, 'parentId' => 3, 'name' => '紅蘋果'],
    6 => ['id' => 6, 'parentId' => 2, 'name' => '進口奶粉'],
    7 => ['id' => 7, 'parentId' => 6, 'name' => '澳洲進口奶粉'],
    8 => ['id' => 8, 'parentId' => 0, 'name' => '3C 產品'],
];

$tree = [];

// 這裡使用 &$item,當 $item 被加入子元素時,也會影響到原本 $array 中對應的 item
foreach ($array as &$item) {
    // 判斷父元素是否存在
    if (isset($array[$item['parentId']])) {
        // 存在,將該元素的引用加入至父元素的 children 陣列
        $array[$item['parentId']]['children'][] = &$item;
    } else {
        // 不存在,將該元素的引用加入至根陣列
        $tree[] = &$item;
    }
}

// 使用 JSON 查看轉換為多維陣列的結果
echo json_encode($tree, JSON_UNESCAPED_UNICODE);

參考資料


PHP: What References Are - Manual
php中引用&的用法分析【變數引用,函式引用,物件引用】 | 程式前沿 (codertw.com)

sharkHead
written by
sharkHead

後端工程師, PHP 基金會每月 5 鎂小額贊助人 稍微擅長 PHP、Python 與 Google Search,偶爾寫寫 TypeScript 對於逗號後面必須加空格有著絕對的堅持