最近更新: 2007-01-19

PHP 的參照及唯讀參照之實作

Tags: magic_method 動態語言

這幾天我和 HACGIS (トキメキ) 在討論 PHP 的參照 (reference) 特性。對於參照的功用,我想我們都很清楚了,還不了解的讀者可以先參閱《PHP Manual::Chapter 21. References Explained》以及 HACGIS 的《使用參照的幾個原則》,HACGIS 的文章是本文討論內容的起點。

基於過去使用 C/C++ 之參照的經驗,我對於 PHP 的參照功能提出兩點個人認為可以改進的地方。

  1. 不能宣告唯讀參照。例如:function abc(const &$a);
  2. 當值為常數時,不會自動建立參照。例如直接傳字串的情形:abc('hello');

唯讀參照的定義

何為「唯讀參照」?我引用 C/C++ 的定義,英文寫作 read-only reference , C/C++ 則寫作 const& (我在回應 HACGIS 時昏了頭,弄混了參照和指標而說「const type& 是指向唯讀內容的參照,type const&是唯讀的參照」。這段話是錯的,對 C/C++ 的參照而言,型態寫在 const 前或後都是一樣意義。在 C/C++ 中,參照本身是唯讀的,是一個隱蔽常數,不需要使用 const 修飾)。參照是指一個符號參考另一個符號而指涉一實體。按 PHP 的說法,參照是為一個變數內容取一個別名,使不同的符號名稱指涉同一變數內容 (PHP Manual: References in PHP are a means to access the same variable content by different names)。那麼參照冠上 const 關鍵字是什麼意思呢?這是修飾參照的對象之內容不變、唯讀之意,令使用者不可透過參照變更變數內容。

我和 HACGIS 對於唯讀參照的看法有所不同。此一看法起因於「唯讀參照是否牽涉到記憶體回收機制?」 HACGIS 認為會,而我認為不會。此一觀點差異,似乎與我們二人對唯讀參照的規格需求不同有關。對我而言,我僅僅認為唯讀參照是一個只可用 getter 而不可用 setter 的參照。因此使用者可以直接地變更變數內容,但不能透過參照去變更內容。但 HACGIS 認為「要考慮PHP在執行階段對於該參照所指涉的變數的readonly特性……對於所指涉的變數內容,必須在某個範圍內保證其不變性」,他希望唯讀參照的參照對象在某個範圍內也是唯讀的。這規格需求超出我所期望者。

唯讀參照的實作

為了進一步討論這個問題,我用 PHP 實踐一個唯讀參照類別。因為 PHP5 只能覆載 setter 和 getter ,所以現階段用 PHP 可以「有條件地」實踐我要的唯讀參照。條件是參照對象為關聯陣列 (associative array) 或個體 (object) 。在本文中,我僅實踐參照對象為個體的參照類別。

<?php
class Reference {
    protected $_target;
    public function refer(&$target) {
        $this->_target = &$target;
    }
    public function __construct(&$target = NULL) {
        if ($target !== NULL) {
            $this->refer($target);
        }
    }
    public function __set($property, $v) {
        if ($this->_target === NULL) {
            throw new Exception('無參照對象!');
        }
        else {
            $this->_target->$property = $v;
        }
    }
    public function __get($property) {
        return $this->_target->$property;
    }
}

class ConstReference extends Reference {
    public function __set($k, $v) {
        throw new Exception('read-only reference!');
    }
}

$x->value = 0;

function showX() {
    global $x;
    echo "\n", '$x is ';
    print_r($x);
}
showX();

$ref = new Reference($x);
$constRef = new ConstReference($x);
$ref2 = new Reference($constRef);

echo "\n---\n(ref) Value of x is {$ref->value} \n";
try {
    $ref->value = 100;
}
catch (Exception $e) {
    echo $e;
}
showX();

echo "\n---\n(constRef) Value of x is {$constRef->value} \n";
try {
    $constRef->value = 200;
}
catch (Exception $e) {
    echo $e;
}
showX();

echo "\n---\n(ref2) Value of x is {$ref2->value} \n";
try {
    $ref2->value = 300;
}
catch (Exception $e) {
    echo $e;
}
showX();
?>

HACGIS 則提問:「那麼我若用另一參照指涉過去,此參照是否亦為唯讀參照?繼承此唯讀參照的物件又如何?」 關於參照指涉的問題不難解釋,參照只是變數內容的別名,一個變數內容當然可以有很多參照。當一個變數有許多參照時,透過一般參照才允許更動變數內容,但透過唯讀參照時不允許。再說到參照指涉另一個參照,尤其是 HACGIS 所問的一般參照指涉唯讀參照的情形。這個問題請看 $ref2 那一段程式碼。 $ref2 是一般參照,允許變更變數內容,但它參照的 $constRef 卻是一個唯讀對象。因此雖然調用 $ref2 的 setter 時不會發生例外,但 $ref2 的 setter 要再調用參照對象 $constRef 的 setter 時,就會吃 $constRef 的閉門羹。

關於繼承的情形,則需要實踐一個「解除唯讀之唯讀參照」類別以便說明。

<?php
class DeconstReference extends ConstReference {
    public function __set($property, $v) {
        $this->_target->$property = $v;
    }
}

$deconstReferenceRef2 = new DeconstReference($ref2);
echo "\n---\n(deconstReferenceRef2) Value of ref2 is {$deconstReferenceRef2->value} \n";
try {
    $deconstReferenceRef2->value = 400;
}
catch (Exception $e) {
    echo $e;
}
showX();

$deconstReferenceX = new DeconstReference($x);
echo "\n---\n(deconstReferenceX) Value of x is {$deconstReferenceX->value} \n";
try {
    $deconstReferenceX->value = 500;
}
catch (Exception $e) {
    echo $e;
}
showX();
?>

我發覺 $deconstReference 參照 $ref2 的連續參照例子很有趣,可以觀察參照串列中各個體行為的調用順序。將其結果列於下:

(deconstReferenceRef2) Value of ref2 is 100
exception 'Exception' with message 'read-only reference!' in 參照.php:27
Stack trace:
#0 參照.php(17): ConstReference->__set('value', 400)
#1 參照.php(74): Reference->__set('value', 400)
#2 參照.php(81): DeconstReference->__set('value', 400)
#3 {main}
$x is stdClass Object
(
    [value] => 100
)

---
(deconstReferenceX) Value of x is 100

$x is stdClass Object
(
    [value] => 500
)

結論

HACGIS 認為「不只是最近的動態語言,像有些 C/C++ 的編譯器,有時需要 2pass 以上去在編譯時確定指標或參照的特性,來決定語法是否正確。所以有些語法,在我們從源碼的角度去審視好像很簡單,但是實做起來就要對各種定義和邊際效益去較真,有時對速度而言是得不償失。」 我以為 HACGIS 對編譯器/解譯器的要求較嚴格,希望它儘可能幫助程序員找出不當語意。但我對此要求較寬鬆。以 DeconstReference 類別為例,其語法合法,而語意在解除 ConstReference 對 setter 所加的禁止事項。然而繼承原則應該是「特化」而非「泛化」,所以 ConstReference 繼承 Reference 就是為了特化 setter 的用途,使 setter 不能作用。但 DeconstReference 繼承 ConstReference 卻是為了解除此一特化而返祖化,明顯違背繼承原則。我不要求語言防範這種捅馬蜂窩的行為,因為這不關語言的事,而是程序員的觀念。我喜歡語言允許這種隨興思考。

本文的實作顯示, PHP5 可以寬鬆態度實踐唯讀參照。如果此一態度下的唯讀參照不是以類別型式而能以語言特性型式實踐,我想效能會更好。但若要以嚴謹態度,令唯讀參照防範程序員的解唯讀動作,則情況就如 HACGIS 所說將會複雜許多。

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

樂多舊回應
HACGIS@gmail.com(tokimeki) (#comment-3865570)
Sat, 20 Jan 2007 03:51:55 +0800
我承認我是過份要求了,不過你說的捅馬蜂窩的行為,我個人倒是常常想去幹 ^^

我個人對編譯器/解譯器的嚴格要求倒是其來有自:我對程式語言總有美好的幻想。

自從ZE2大量模仿JAVA以及看到PHP6的討論文後,我老是覺得「通用程式語言」就這樣了嗎?

Ruby曾讓我眼睛一亮,不過很快我就對他不耐煩了,Perl和Python那就更不用說了,這只是我個人的直觀,不代表這些語言的好壞,請不要對號入座。
未留名 (#comment-3870932)
Mon, 22 Jan 2007 15:29:46 +0800
我想應該沒有通用程式語言這玩意,就算有,也是 C/C++ 夠資格,輪不到 Java 。我從來就無法欣賞 Java 語言。(JVM 倒真的不錯)
自從 Java 1.0 版很認真地玩了一陣子後,後來就幾乎沒碰了,因為要嘛就是我已經有 C/C++ 寫好的,要嘛用 PHP 更快完成工作。

Ruby 跟 Python 還不錯,它們的語法有許多優於 PHP 之處。但這些優點,還無法完全轉換成生產力。Rbuy 和 Python 是我在學習中的兩套語言與工具。學習中語言所提供之生產力,還無法趕上我已經熟練的 PHP 。畢竟它們的區別不像 C/C++ 與 PHP 之間那麼大。