最近更新: 2007-07-19

TWPUG問答 - PHP5 個體指派動作的陷阱

前幾天在 TWPUG 上,有位網友提了一個問題。大意是如何以一個個體為正本,透過指派動作複製多次到陣列中,每個陣列元素的內容應該不相同。我看出他碰到了一個語言陷阱,我也回答了。可惜,我當時的答案是錯的... 我重新思索了一下,本文才是正解。

在 PHP5 之後,個體(object)的指派動作皆是使用參照。換言之,當指派來源的資料型態是object時, PHP5 就會用參照;故 $a = $o 的動作實際上等於 $a =&$o

As of PHP 5, objects are assigned by reference unless explicitly told otherwise with the new clone keyword. PHP Manual: Assignment Operators

如果我們希望透過指派動作得到複製體,則必須使用關鍵字 clone 明確地告知 PHP5 產生一個複製體(右值的複製體)指派給對象(左值)(PHP5 預設的複製動作是淺拷貝(shallow copy)。可覆寫__clone()自定複製內容。See also: PHP Manual: Object cloning)。如下列所示。

<?php
/*
 * 1. normal assign
 */
$o = new StdClass; //equal: $o = (object)null;
$ar = array();

for ($i = 0; $i < 3; ++$i) {
    $o->a = $i; //print_r($o);
    $ar[] = $o; //print_r($ar);
}

foreach ($ar as $a)
    echo $a->a, "\n";

/*
 * 2. assign by clone
 */
$o = new StdClass;
$ar = array();

for ($i = 0; $i < 3; ++$i) {
    $o->a = $i; //print_r($o);
    $ar[] = clone $o; //print_r($ar);
}

foreach ($ar as $a)
    echo $a->a, "\n";

?>

上列範例中,先使用一般的指派方式,結果陣列 $ar 中的每個元素都相同,其實都參照變數 $o。接著改用加上關鍵字 clone 的指派方式,這才使得陣列 $ar 中的每個元素都是一個獨立的個體,內容各不相同。

其他型態的指派動作仍然是「複製」

請留意, PHP5 仍非完全個體導向化的程式語言,並未將每個變數都視為一種個體。在 PHP5 中,「個體」僅指資料型態為 object (基礎類別為 StdClass) 的變數。布林、數值、字串及陣列等,各有其獨立的資料型態,如boolean, integer, string, array。它們與 object 是同層級的資料型態,不被視為「個體」(PHP5有8種資料原生型態,它們是平行非附屬的關係,故一個array的實例不是一種(is not a ) object的實例)。詳細內容請參考 PHP Manual: Types

PHP5 對 object 型態的變數之指派動作預設是「參照」,但對其他型態的變數之指派動作仍然是「複製」。亦即,當右值之資料型態不是 object 時, PHP5 就會複製一個獨立的內容指派給左值。

下列示範當 $s 之資料型態分別是 stringobject 時,PHP5 指派動作產生的結果差異。

<?php
$s = 'Hello';
$ar = array();
for ($i = 0; $i < 3; ++$i) {
    $s .= $i;
    $ar[] = $s;
}
$s .= 'x';
print_r($ar);

$s = (object) 'Hello';
$ar = array();
for ($i = 0; $i < 3; ++$i) {
    $s->scalar .= $i;
    $ar[] = $s;
}
$s->scalar .= 'x';
print_r($ar);
?>

先令 $s 之資料型態為字串 string ,再指派給陣列 $ar。結果顯示 $ar 各元素內容皆不相同,並非參照 $s。接著令 $s 之資料型態為 object ,再指派一次。結果顯示 $ar 各元素內容相同,其實都參照 $s

基本上,這是一個語言陷阱(trick)。再者,若將上例第6行的指派動作加上 clone 關鍵字,將會產生非預期的結果。同時, PHP 會警告程序員:Warning: __clone method called on non-object. 此意味著,程序員編寫之敘述必須區別個體與非個體資料型態。關鍵字 clone 僅作用於右值為 object 型態之變數,故而降低了 PHP 的泛型能力。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/3686877.html

樂多舊回應
未留名 (#comment-11609289)
Fri, 20 Jul 2007 09:31:39 +0800
PHP5 的 clone 是 shallow copy ,這點手冊有提到。

所以我們必須實作 __clone() 來複製個體裡其他的內在變數,也就是 deep copy 。
未留名 (#comment-11609333)
Fri, 20 Jul 2007 09:40:39 +0800
呃...上面的是補充而已,跟內容沒什麼太大關係。
HACGIS@gmail.com(tokimeki) (#comment-11609377)
Fri, 20 Jul 2007 09:47:22 +0800
那段程式超出我撰寫PHP的習慣,在非常明顯的情況下,我個人是不會把陣列使用成物件~

所以老實說,他為何要那樣寫,我不是很理解~
HACGIS@gmail.com(tokimeki) (#comment-11609419)
Fri, 20 Jul 2007 09:56:00 +0800
剛剛又重新看了一下,發現是我看錯...
我把 $cust 跟 $custs 看成同一個,如果是這樣的話,其實問題就簡單,那是 PHP5 的特性,我在這裡:
http://blog.pixnet.net/HACGIS/post/1880532
已經作過討論~
未留名 (#comment-11622447)
Fri, 20 Jul 2007 11:48:33 +0800
其實那段程式的作法也算常用。

例如我現在要產生多筆紀錄,而這些紀錄中可能只有一、兩項資料不同。如果我還要每筆紀錄都重新配置一個object,效率很差,也會重覆相似的程式碼。

那麼我就會想只配置一個object,然後每次只改一、兩項資料,再把這個object目前的內容複製到陣列。如此便不需要重覆重新配置並填入每一項資料值的動作。

亦即,他的需求是複製,而不是參照。

在C/C++中,這點倒不會混淆。因為指派動作的意義就是複製 (覆載 operator= 或 copy constructor 決定複製程度之深淺)。只有加上參照算符時,才會用參照。

其實 PHP4 也不會混淆...
racklin+blogger@gmail.com(racklin) (#comment-12232329)
Sun, 22 Jul 2007 13:04:05 +0800
猜測原發問者可能是要模擬類似 ORM (http://en.wikipedia.org/wiki/Object-relational_mapping) 的功能. 這在 java programmer 大部份都會利用 spring / ibatis / hibernate 這麼做.
只是, 它沒有合理的為每一個 cust 擁有一個自己 instance.
For ORM issue, 會為每一個 object 配置一個 instance, 通常它們只是一個資料的封裝(POJO ,http://en.wikipedia.org/wiki/POJO).
所以, 他原意可能希望的 custs 代表 cust object array, 只是 cust object 沒有 new cust() and assign data.
未留名 (#comment-12685605)
Tue, 24 Jul 2007 17:04:05 +0800
原發問者的原意,是想用 operator= 或 copy constructor 一次完成 new and assign 的動作。喔,我記得 Java2 的 assign 動作是用參照,沒有 operator= 和 copy constructor 的概念,這一點跟 C/C++ 不同。

PHP4 時代, StdClass::operator= 的用法像 C/C++ 。PHP5 把 StdClass::operator= 的動作改成像 Java 一樣用 reference ,所以無法達成他要的效果。

原發問者也提到有些主機可以,有些主機不行。顯然,有些主機安裝的是 PHP4 ,所以可用;有些主機裝 PHP5 ,故不可用。