開始瞭解 PHP V5 中的對象

開始瞭解 PHP V5 中的對象
內容:
什麼是類和對象?
第一個類
屬性
方法
構造函數
關鍵字:在此我們是否可以有一點隱私?
在類上下文操作
繼承
結束語
參考資料
關於作者
對本文的評價
相關內容:
審計 PHP,第 1 部分: 理解 register_globals
PHP 簡介
PHP by example, Part 1
訂閱:
developerWorks 時事通訊
爲什麼需要了解對象、類和如何使用它們

級別: 初級

Matt Zandstra
開發人員和作家, Yahoo!
2005 年 6 月 20 日

本文描述 PHP V5 中對象和類的基礎知識,從最基本的概念一直講到繼承,主要針對經驗豐富的面向對象程序員和尚未接觸過對象的讀者。

作爲 PHP 程序員,您肯定知道變量和函數。但類和對象可能就是另一回事。不定義單個類,就可以創建完美的系統。但即使您決定在自己的代碼中不使用面向對象的編程,您仍可能需要了解面向對象的編程。例如,如果使用第三方庫,比如通過 PHP Extension and Application Repository (PEAR) 可以使用的庫,您將發現自己在實例化對象和調用方法。

什麼是類和對象?
簡單地說, 是一個由變量和方法組成的獨立塊或束。這些組件通常結合實現單個責任或一組責任。在本文中,您將創建一個類,該類收集了用於查詢和填充由項和值組成的詞典的方法。

類可以直接用作組織數據和功能的簡單方法,就像一組函數和變量一樣。但使用類可以忽略它的存在。類可用於在內存中生成多個實例。這樣的實例叫做對象。每個對象可以訪問一組相同的函數(在面向對象上下文中叫做方法)和變量(叫做特性實例變量),但每個變量的實際值在每個對象中是不同的。

考慮角色扮演遊戲中的一個單元——比如坦克。類可能爲坦克設置一組變量:防禦和進攻能力,範圍,健康狀況,等等。該類也可能定義一組函數,其中包括 move()attack()。當系統包含一個坦克類時,該類可用於生成數十個或數百個坦克對象,每個對象都潛在地具有自己的健康狀況或範圍特徵。因此,類是用於生成對象的藍圖或模板。

理解類和對象最簡單的方法可能就是創建一些類和對象。

第一個類
可以用 class 關鍵字創建類。最簡單的情況是,類由關鍵字類、名稱和代碼塊組成:


class Dictionary {

}

類名可以包含字母、數字和下劃線字符的任何組合,但不能以數字打頭。

上例中的 Dictionary 類儘管用處有限,但完全合法。那麼如何使用該類來創建一些對象呢?


$obj1 = new Dictionary();
$obj2 = new Dictionary();
$obj3 = new Dictionary();

至少在形式上,實例化對象與調用函數相似。對於函數調用,必須提供圓括號。與函數一樣,一些類需要您爲其傳遞參數。您還必須使用 new 關鍵字。這就告訴 PHP 引擎您希望實例化一個新對象。然後,返回的對象可以存儲在一個變量中以供將來使用。

屬性
在類的主體中,可以聲明叫做屬性的特殊變量。在 PHP V4 中,屬性必須用關鍵字 var 調用。這仍是合法的語法,但主要是爲了向後兼容。在 PHP V5 中,屬性必須聲明爲 public、private 或 protected。可以在 關鍵字:在此我們是否可以有一點隱私?中閱讀有關這些限定詞的內容。但現在在例子中將所有屬性聲明爲 public。清單 1 顯示一個聲明瞭兩個屬性的類。

清單 1. 聲明兩個屬性的類

class Dictionary {
    public $translations = array();
    public $type ="En";
}

正如所看到的,可以同時聲明屬性併爲其賦值。可以用 print_r() 函數快速瀏覽一下對象的狀態。清單 2 顯示 Dictionary 對象現在具有更多成員。

清單 2. Dictionary 對象一覽

$en = new Dictionary();
print_r( $en );  

如果運行該腳本,將看到如下對象的輸出:

Dictionary Object
(
    [translations] => Array
        (
        )

    [type] => En
)

可以使用對象操作符 -> 訪問公共對象屬性。所以 $en->type 表示由 $en 引用的 Dictionary 對象的 $type 屬性。如果可以訪問屬性,就意味着可以設置和獲得其值。清單 3 中的代碼創建 Dictionary 類的兩個實例 —— 換言之,它實例化兩個 Dictionary 對象。它更改一個對象的 $type 屬性,並添加兩個對象的翻譯:

清單 3. 創建 Dictionary 類的兩個實例

$en = new Dictionary();
$en->translations['TREE'] = "tree";

$fr = new Dictionary();
$fr->type = "Fr";
$fr->translations['TREE'] = "arbre";

foreach ( array( $en, $fr ) as $dict ) {
    print "type: {$dict->type} ";
    print "TREE: {$dict->translations['TREE']}/n";
}

該腳本輸出如下


type: En TREE: tree
type: Fr TREE: arbre

所以 Dictionary 類現在比較有用了。單個對象可以存儲不同的鍵值組合,還有一個標誌,該標誌告訴客戶端有關這種 Dictionary 的詳細信息。

儘管 Dictionary 類當前與關聯數組的包裝器相差無幾,但這裏有一些瞭解對象功能的線索。目前,我們已經可以很好地表示我們的示例數據了,如清單 4 所示。

清單 4. 示例數據

$en = array(
    'translations'=>array( 'TREE' => 'tree' ),
    'type'=>'En'
);

$fr = array(
    'translations'=>array( 'TREE' => 'arbre' ),
    'type'=>'Fr'
);

雖然該數據結構完成了與 Dictionary 類相同的目的,但它沒有提供結構的保證。如果傳遞 Dictionary 對象,我們知道它具有 $translations 屬性。但如果是一個關聯數據,則沒有這樣的保證。這個事實使得類似 $fr['translations']['TREE']; 的查詢有些碰運氣,除非進行查詢的代碼確定數組的起源。這是對象的重點:對象的類型是其特徵的保證。

雖然用對象存儲數據有優點,但是您可能沒有一點感覺。對象可以是東西,但關鍵在於它們還可以做事情。

方法
簡單地說,方法是在類中聲明的函數。它們通常(但不總是)通過對象實例使用對象操作符來調用的。清單 5 向 Dictionary 類中添加一個方法,並調用該方法。

清單 5. 向 Dictionary 類中添加方法

class Dictionary {
    public $translations = array();
    public $type ="En";

    function summarize() {
        $ret  = "Dictionary type: {$this->type}/n";
        $ret .= "Terms: ".count( $this->translations )."/n";
        return $ret;
    }
}

$en = new Dictionary();
$en->translations['TREE'] = "tree";
print $en->summarize();

它提供如下輸出:


Dictionary type: En
Terms: 1

正如所看到的,聲明 summarize() 方法與聲明任何函數的方式一樣,只不過它是在類中聲明。summarize() 方法是通過 Dictionary 實例使用對象操作符調用的。summarize() 函數訪問屬性來提供對象狀態的簡述。

注意對於本文來說的一個新特性的用法。$this 僞變量提供了一種用於對象引用自己的屬性和方法的機制。在對象外部,可以使用句柄來訪問它的元素(在本例子中是 $en)。在對象內部,則無此句柄,所以必須求助於 $this。如果覺得 $this 有些迷惑,則在代碼中遇到它時,試着在頭腦中用當前實例 替換它。

類通常使用通用建模語言 (Universal Modeling Language,UML) 表示在圖表中。UML 的詳細信息超出了本文的範圍,但這種圖表不過是一種可視化類關係的好方法。圖 1 顯示用 UML 表示的 Dictionary 類。類名位於頂層,屬性在中間,方法在底層。

圖 1. 使用 UML 顯示 Dictionary 類
使用 UML 顯示 Dictionary 類

構造函數
PHP 引擎識別許多“魔術”方法。如果定義了方法,則 PHP 引擎將在相應的情況發生時自動調用這些方法。最常實現的方法是構造函數方法。PHP 引擎在實例化對象時調用構造函數。對象的所有基本設置代碼都放在構造函數中。在 PHP V4 中,通過聲明與類同名的方法來創建構造函數。在 V5 中,應聲明叫做 __construct() 的方法。清單 6 顯示需要 DictionaryIO 對象的構造函數。

清單 6. 需要 DictionaryIO 對象的構造函數

class Dictionary {
    public $translations = array();
    public $type;
    public $dictio;

    function __construct( $type, DictionaryIO $dictio ) {
        $this->type = $type;
        $this->dictio=$dictio;
    }

    //...

要實例化 Dictionary 對象,需要將類型字符串和 DictionaryIO 對象傳遞給它的構造函數。構造函數使用這些參數來設置自有屬性。下列代碼顯示可以如何實例化 Dictionary 對象:


$en = new Dictionary( "En", new DictionaryIO() );

Dictionary 類現在比以前更安全。所有 Dictionary 對象都已經用必需的參數初始化過了。

當然,還無法阻止一些人隨後更改 $type 屬性或將 $dictio 設置爲空。可喜的是,PHP V5 可以幫助您實現這一功能。

關鍵字:在此我們是否可以有一點隱私?
前面已經看到與屬性聲明相關的 public 關鍵字。該關鍵字表示屬性的可見度。事實上,屬性的可見度可以設置爲 public、private 和 protected。聲明爲 public 的屬性可以在類外部寫入和讀取,聲明爲 private 的屬性只在對象或類上下文中可見。聲明爲 protected 的屬性只能在當前類及其子類的上下文中可見。(在 繼承 部分將會看到這些內容起作用。)可以使用 private 屬性來真正鎖定類。如果將屬性聲明爲 private 並試圖從類範圍外部訪問它(如清單 7 所示),PHP 引擎將拋出致命錯誤。

清單 7. 試圖從類範圍外部訪問屬性

class Dictionary {
    private $translations = array();
    private $dictio;
    private $type;

    function __construct( $type, DictionaryIO $dictio ) {
        $this->type = $type;
        $this->dictio = $dictio;
    }

    // ...
}

$en = new Dictionary( "En", new DictionaryIO() );
$en->dictio = null;

輸出如下:


Fatal error: Cannot access private property 
Dictionary::$dictio in...

一般來說,應將大多數屬性聲明爲 private,然後根據需要提供獲得和設置這些屬性的方法。這樣就可以控制類的接口,使一些數據只讀,在將參數分配給屬性之前對參數進行清理或過濾,並提供與對象交互的一套明確的規則。

修改方法可見度的方法與修改屬性可見度的方法一樣,即在方法聲明中添加 public、private 或 protected。如果類需要使用一些外部世界無需知道的家務管理方法,則可以將其聲明爲 private。在清單 8 中,get() 方法爲 Dictionary 類的用戶提供了提取翻譯的接口。該類還需要跟蹤所有查詢,因此提供了 private 方法 logQuery()

清單 8. get() 方法爲 Dictionary 類的用戶提供了接口

function get( $term ) {
    $value = $this->translations[$term];
    $this->logQuery( $term, $value, "get" );
    return $value;
}

private function logQuery( $term, $value, $kind ) {
    // write log information
}

logQuery() 聲明爲 private 簡化了公共接口,而且防止了類不適當地調用 logQuery()。與屬性一樣,嘗試從包含類外部調用私有方法將導致致命錯誤。

在類上下文操作
到目前爲止,所看到的方法和屬性都在對象上下文中進行操作。也就是說,必須使用對象實例,通過 $this 僞變量或標準變量中存儲的對象引用來訪問方法和屬性。有時候,可能發現通過類而不是對象實例來訪問屬性和方法更有用。這種類成員叫做靜態 成員。

要聲明靜態屬性,將關鍵字 static 放在可見度修飾符後面,直接位於屬性變量前面。

下例顯示單個靜態屬性:$iodir,存放用於保存和讀取 Dictionary 數據的默認目錄的路徑。因爲該數據對於所有對象是相同的,所以讓它可用於所有實例是有意義的。

清單 9. 單個靜態 $iodir 屬性

class Dictionary {
    public static $iodir=".";
    // ...
}

可以使用範圍解析操作符來訪問靜態屬性,該操作符由雙冒號 (::) 組成。範圍解析操作符應位於類名和希望訪問的靜態屬性之間。


print Dictionary::$iodir . "/n";
Dictionary::$iodir = "/tmp";

正如所看到的,訪問該屬性無需實例化 Dictionary 對象。

聲明和訪問靜態方法的語法與此相似。再次,應將 static 關鍵字放在可見度修飾符後。清單 10 顯示了兩個靜態方法,它們訪問聲明爲 private 的 $iodir 屬性。

清單 10. 訪問 $iodir 屬性的兩個靜態方法

class Dictionary {
    private static $iodir=".";
    // ...
    public static function setSaveDirectory( $dir ) {
        if ( ! is_dir( $dir ) || 
             ! is_writable( $dir ) ) {
            return false;
        }
        self::$iodir = $dir;
    }

    public static function getSaveDirectory( ) {
        return self::$iodir;
    }
    // ...
} 

用戶不再能訪問 $iodir 屬性目錄了。通過創建特殊方法來訪問屬性,可以確保所提供的任何值是健全的。在本例中,方法在進行分配前檢查給定字符串指向可寫入的目錄。

注意,兩個方法都使用關鍵字 self 和訪問解析操作符來引用 $iodir 屬性。不能在靜態方法中使用 $this,因爲 $this 是對當前對象實例的引用,但靜態方法是通過類而不是通過對象調用的。如果 PHP 引擎在靜態方法中看到 $this,它將拋出致命錯誤和一條提示消息。

要從類外部調用靜態方法,可使用類名加上範圍解析符和方法名。


Dictionary::setSaveDirectory("/tmp");
print Dictionary::getSaveDirectory();

需要使用靜態方法有兩個重要原因。首先,實用程序操作可能不需要對象實例來做它的工作。通過聲明爲靜態,爲客戶機代碼節省了創建對象的工作量。第二,靜態方法是全局可用的。這意味着可以設置一個所有對象實例都可以訪問的值,而且使得靜態方法成爲共享系統上關鍵數據的好辦法。

儘管靜態屬性通常被聲明爲 private 來防止別人干預,但有一種方法可以創建只讀靜態範圍的屬性,即聲明常量。與全局屬性一樣,類常量一旦定義就不可更改。它用於狀態標誌和進程生命週期中不發生更改的其他東西,比如 pi 或非洲的所有國家。

const 關鍵字聲明類常量。例如,因爲 Dictionary 對象的實際實現背後幾乎肯定有一個數據庫,所以還可以假設項和翻譯有最大長度。清單 11 將其設置爲類常量。

清單 11. 將 MAXLENGTH 設置爲類常量

class Dictionary {
    const MAXLENGTH = 250;
    // ... 
}

print Dictionary::MAXLENGTH;

類常量始終爲 public,所以不能使用可見度關鍵字。這並是問題,因爲任何更改其值的嘗試都將導致解析錯誤。還要注意,與常規屬性不同,類常量不以美元符號開始。

繼承
如果熟悉面向對象編程,您將知道我一直把最好的留到最後。類及其生成的動態對象之間的關係使得系統更靈活。例如,每個 Dictionary 對象封裝不同的翻譯數據集合,但是這些不同實體的模型定義在單個 Dictionary 類中。

但有時候需要記下類級別的差異。是否記得 DictionaryIO 類?扼要重述一下,它從 Dictionary 對象中獲取數據,將其寫入文件系統,從一個文件中獲取數據,將其合併回到 Dictionary 對象中。清單 12 顯示使用序列化來保存和加載 Dictionary 數據的快速實現。

清單 12. 使用序列化的快速實現

class Dictionary {
    // ...

    function asArray() {
        return $this->translations;
    }

    function getType() {
        return $this->type;
    }

    function export() {
        $this->dictio->export( $this );
    }

    function import() {
        $this->dictio->import( $this );
    }
}

class DictionaryIO {

    function path( Dictionary $dictionary, $ext ) {
        $path  = Dictionary::getSaveDirectory();
        $path .= DIRECTORY_SEPARATOR;
        $path .= $dictionary->getType().".$ext";
        return $path;
    }

    function export( Dictionary $dictionary ) {
        $translations = $dictionary->asArray();
        file_put_contents( $this->path(
                           $dictionary, 'serial'), 
                           serialize( $translations ) );  
    }

    function import( Dictionary $dictionary ) {
        $path = $this->path( $dictionary, 'serial' );
        if ( ! is_file( $path ) ) return false; 
        $translations = unserialize( 
                        file_get_contents( $path ) );
        foreach ( $translations as $term => $trans ) {
            $dictionary->set( $term, $trans );
        }
    }
}

$dict = new Dictionary( "En", new DictionaryIO() );
$dict->set( "TREE", "tree" );
$dict->export();

本例引入兩個簡單的 Dictionary 方法,具體來說,asArray() 返回 $translations 數組的副本。DictionaryIO 實現具有簡約的優點。因爲在示例代碼中通常省略了錯誤檢查,即便如此,這仍是將數據保存到文件中的快速簡單的方法。

一旦部署了這種庫之後,則需要立即支持它的保存格式。讓格式過時會冒犯那些可能以這種方式存儲備份的用戶的願望。但要求改變了,而且還可能收到輸出格式不方便用戶編輯的抱怨。這些用戶希望將導出文件以 XML 格式發送給第三方。

現在面臨一個問題。如何在 DictionaryIO 接口中支持兩種格式?

一個解決方案是在 export()import() 方法中使用條件語句,測試類型標誌,如清單 13 所示。

清單 13. 在 export()import() 方法中使用條件語句

function export( Dictionary $dictionary ) {
    if ( $this->type == DictionaryIO::SERIAL ) {
        // write serialized data
    } else if ( $this->type == DictionaryIO::XML ) {
        // write xml data
    }
}

function import( Dictionary $dictionary ) {
    if ( $this->type == DictionaryIO::SERIAL ) {
        // read serialized data
    } else if ( $this->type == DictionaryIO::XML ) {
        // read xml data
    }
}

這種結構是壞“代碼味道”的一個例子,原因在於它依賴於複製。在一個地方進行更改(比如,添加新類型測試)需要在其他地方進行一組相應的更改(將其他類型測試帶入行中),代碼很快就會變得易錯難讀。

繼承提供了更優雅的解決方案。可以創建一個新類 XmlDictionaryIO,該類繼承由 DictionaryIO 設置的接口,但覆蓋其中一些功能。

使用 extends 關鍵字創建子類。如下是 XmlDictionaryIO 類的最小實現:


XmlDictionaryIO extends DictionaryIO {
}

XmlDictionaryIO 現在的功能與 DictionaryIO 完全相同。因爲它從 DictionaryIO 繼承了所有的公共(和保護)屬性,所以可以將應用於 DictionaryIO 對象的相同操作應用於 XmlDictionaryIO 對象。這種關係擴展到對象類型。XmlDictionaryIO 對象顯然是 XmlDictionaryIO 類的實例,但它也是 DictionaryIO 的實例 —— 同樣地,以一般化的順序,一個人同時是人類、哺乳動物和動物。可以使用 instanceof 操作符來測試這一點,如果對象是指定類的成員,則返回 true,如清單 14 所示。

清單 14. 使用 instanceof 操作符測試繼承

$dictio = new XmlDictionaryIO();
if ( $dictio instanceof XmlDictionaryIO ) {
    print "object is an instance of XmlDictionaryIO/n";
}

if ( $dictio instanceof DictionaryIO ) {
    print "object is an instance of DictionaryIO/n";
}

輸出如下:


object is an instance of XmlDictionaryIO
object is an instance of DictionaryIO

正如 instanceof 接受 $dictioDictionaryIO 對象,所以方法也將接受這些對象作爲參數。這意味着 XmlDictionaryIO 對象可以被傳遞給 Dictionary 類的構造函數,即使 DictionaryIO 是由構造函數的簽名指定的類型。

清單 15 是快而髒的 XmlDictionaryIO 實現,使用 DOM 來完成 XML 功能。

清單 15. XmlDictionaryIO 實現

class XmlDictionaryIO extends DictionaryIO {

    function export( Dictionary $dictionary ) {
        $translations = $dictionary->asArray();
        $doc = new DOMDocument("1.0");
        $dic_el = $doc->createElement( "dictionary" ); 
        $doc->appendChild( $dic_el );
        foreach ( $translations as $key => $val ) {
            $term_el = $doc->createElement( "term" );
            $dic_el->appendChild( $term_el );
            $key_el = $doc->createElement("key", $key );
            $val_el = $doc->createElement(
                      "value", $val );
            $term_el->appendChild( $key_el );
            $term_el->appendChild( $val_el );
        }
        file_put_contents( $this->path( 
                           $dictionary, 'xml'), 
                           $doc->saveXML() );
    }

    function import( Dictionary $dictionary ) {
        $path = $this->path( $dictionary, 'xml');
        if ( ! is_file( $path ) ) return false;
        $doc = DOMDocument::loadXML( 
               file_get_contents( $path ) );
        $termlist = $doc
                    ->getElementsByTagName( "term" );
        foreach ( $termlist as $term ) {
            $key = $term->getElementsByTagName( "key" )
                   ->item( 0 )->nodeValue;
            $val = $term
                   ->getElementsByTagName( "value" )
                   ->item( 0 )->nodeValue;
            $dictionary->set( $key, $val ); 
        }
    }
}

有關獲得並生成 XML 的詳細信息是當然要介紹的。有許多方法能完成這一操作,其中包括完美的 SimpleXML 擴展。簡言之,import() 方法以 XML 文檔爲參數,並使用它來填充 Dictionary 對象。export() 方法從 Dictionary 對象中取得數據,並將其寫入 XML 文件中。(在現實世界中,可能會使用叫做 XLIFF 的基於 XML 的格式,該格式適用於導入到第三方翻譯工具中。)

注意,import()export() 都調用實用程序方法 path(),該方法不存在於 XmlDictionaryIO 類中。但沒有關係,因爲 path()DictionaryIO 中實現。當 XmlDictionaryIO 實現一個方法時,則當調用該方法時,會爲 XmlDictionaryIO 對象調用該實現。當沒有任何實現存在時,調用失敗返回給父類。

圖 2 顯示了 DictionaryIOXmlDictionaryIO 類之間的繼承關係。封閉的箭頭表示繼承,從子類指向父類。

圖 2. 繼承關係
繼承關係

結束語
由於篇幅有限,因此不可能全部介紹。進一步研究有兩個方向:廣度和深度。廣度指的是超出本文範圍的那些特性,比如抽象類、接口、迭代器接口、反射、異常和對象複製。深度指的是設計問題。儘管理解 PHP 中可用於面向對象編程的工具範圍很重要,但考慮如何最佳使用這些特性同樣重要。幸運的是,專門講述面向對象上下文中設計模式的可用參考資料很多(參閱“參考資料”)

如果仍在使用 PHP V4 編程,我希望您查找足夠新的特性來證明遷移到 V5 及其面向對象核心特性是正確的。不久您就會驚訝地發現自己無法離開它們了。

參考資料

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文

  • PHP 的所有的面向對象特性類和對象 (PHP V5) 的基本概述可以在官方 PHP 手冊中的 Chapter 19 中找到。

  • 有關抽象類類型問題的文章,請參閱 Working with Class Types: Abstract Classes and Interfaces

  • 有關 PHP V5 錯誤處理的概述,請參閱 Exceptional Code

  • 對面向對象設計有興趣的人要獲得極好的參考資料,請登錄 C2 Wiki

  • PHP 語言主頁以及下載 PHP 的站點是 PHP.net

  • PHP Builder 是流行的 PHP 站點,有教程和代碼庫。

  • Zend.com 包含許多有關 PHP 的有用鏈接和信息。Zend 是 PHP 優化器。

  • 使用 PHP 進行開發時,要查找需要更新的文章、教程、項目信息和新聞,請參閱 developerWorks PHP top projects

  • 登錄 developerWorks 開放源碼專區 獲得廣泛的 how-to 信息、工具和項目更新,以幫助您使用開放源碼技術進行開發,並將它們與 IBM 的產品一起使用。

  • IBM 試用軟件 革新您的下一個開放源碼開發項目,可以下載該軟件或用 DVD 安裝。

  • 在 Developer Bookstore 的開放源碼專區可以找到數百本 開放源碼主題的打折書籍,其中包括許多 PHP 書籍

  • 通過參與 developerWorks blogs 加入 developerWorks 社區。

關於作者
Matt Zandstra 在過去 10 多年裏是專攻互聯網應用程序的開發人員、教師和作家。他是 Yahoo 的工程師,他在這裏幫助開發核心模板管理系統。他是 SAMS Teach Yourself PHP in 24 Hours 的作者,最近出版了 PHP 5: Objects, Patterns and Practice。他曾爲 Linux Magazine、Zend.com 和 bgz-consultants.com 寫過 PHP 方面的文章。
發佈了35 篇原創文章 · 獲贊 0 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章