2011-09-19 43 views
10

我學習PHP類和異常,而且,從C來++背景,以下令我奇怪:範圍展開的PHP類的構造函數

當一個派生類的構造函數拋出異常,它出現基類的析構函數不會自動運行:

class Base 
{ 
    public function __construct() { print("Base const.\n"); } 
    public function __destruct() { print("Base destr.\n"); } 
} 

class Der extends Base 
{ 
    public function __construct() 
    { 
    parent::__construct(); 
    $this->foo = new Foo; 
    print("Der const.\n"); 
    throw new Exception("foo"); // #1 
    } 
    public function __destruct() { print("Der destr.\n"); parent::__destruct(); } 
    public $foo;     // #2 
} 

class Foo 
{ 
    public function __construct() { print("Foo const.\n"); } 
    public function __destruct() { print("Foo destr.\n"); } 
} 


try { 
    $x = new Der; 
} catch (Exception $e) { 
} 

此打印:

Base const. 
Foo const. 
Der const. 
Foo destr. 

另一方面,如果在構造函數中有異常(在#1),成員對象的析構函數是。現在我想知道:如何在PHP中的類層次結構中實現正確的範圍展開,以便在發生異常時正確銷燬子對象?

而且,似乎沒有辦法運行的基礎析構函數所有成員對象已被銷燬(在#2)。要曉得,如果我們刪除線#1,我們得到:

Base const. 
Foo const. 
Der const. 
Der destr. 
Base destr. 
Foo destr. // ouch!! 

一個將如何解決這一問題?

更新:我仍然願意做出進一步的貢獻。如果有人有充分的理由說明爲什麼PHP對象系統永遠不需要需要一個正確的銷燬序列,那麼我會給出另一個賞金(或者僅僅爲了任何其他令人信服的爭論答案)。

+1

我必須說我很少需要在PHP中實現析構函數,所以也許這不是什麼大問題。不過你確實提出了一個很好的問題。 –

+0

@Jani:坦率地說,考慮到它們的設計方式,我明白你爲什麼不想*使用析構函數。我只是想知道他們爲什麼看起來如此糟糕,以及除了「不要使用這部分語言」之外,是否有任何常見的成語來規避這種設計缺陷?:-S –

+1

同意Jani:在PHP中編寫析構函數確實毫無意義,因爲沒有任何東西可以泄漏。來自C++,你可能將析構函數視爲一個非常深思熟慮的工具,用於解決他們無意解決的問題。 – Jon

回答

6

我想解釋爲什麼PHP的行爲是這樣,爲什麼它實際上使一些感覺。

在PHP 中,只要沒有更多引用,就立即銷燬對象。參考可以以多種方式被移除,例如,由unset()變量,通過保留範圍或作爲關機的一部分。

如果你明白這一點,你可以很容易地理解這裏發生了什麼(我將不例外首先解釋的情況下):

  1. PHP進入關斷,因此,所有的變量引用都被刪除。
  2. 當刪除由$x(對於Der的實例)創建的引用時,該對象被銷燬。
  3. 調用派生析構函數,調用基本析構函數。
  4. 現在從$this->fooFoo實例的引用被移除(如破壞成員字段的一部分。)
  5. 有沒有Foo任何更多的參考資料要麼,所以它被摧毀也和析構函數被調用。

想象一下,在調用析構函數之前,這不會以這種方式工作,並且成員字段將被銷燬:在析構函數中無法再訪問它們。我嚴重懷疑在C++中有這樣的行爲。

在異常情況下,您需要明白,對於PHP,從來沒有真正存在類的實例,因爲構造函數從未返回。你怎麼能摧毀那些從未構建過的東西?


如何解決?

你不知道。事實上,你需要一個析構函數可能是一個糟糕的設計的標誌。事實上,銷燬順序對你來說很重要,甚至更多。

+0

我不確定我是否購買了這個解釋:當我在例子中說'ouch'時,這是因爲我預計破壞序列是「derived - foo - base」。但是這當然不會發生,因爲我實際上明確調用了基本析構函數。但想象一下'$ this-> foo'對象依賴於'Base'子對象的有效狀態。現在'$ this-> foo'的銷燬可能需要執行一些需要'Base'子對象的關閉,但這不再有效。有沒有原因爲什麼這不能或不應該發生? –

+0

@KerrekSB我已經解釋過,成員需要在調用析構函數後銷燬,否則無法在析構函數中訪問它們。這與C++(afaik)中的相同。此外:$ this-> foo *不能取決於「Base」對象(它應該如何訪問它?)。 '$ this-> foo'是一個依賴項(應該注入,參見DI,IoC和SOLID)。它甚至不應該知道它被用在另一個類中,並且絕對不會依賴它。 – NikiC

+0

基本和派生對象之間的區別很重要。事實上,派生的析構函數必須先來。但是'$ this-> foo'是在構造基礎子對象之後構建的,所以它應該在基礎之前被銷燬*我會在帖子中添加一個示例! –

2

這不是一個答案,而是對問題動機的更詳細的解釋。我不想用這種有點切題的材料來混淆這個問題。

這裏是我如何預期派生類與成員的通常銷燬序列的解釋。假設類是這樣的:

class Base 
{ 
    public $x; 
    // ... (constructor, destructor) 
} 

class Derived extends Base 
{ 
    public $foo; 
    // ... (constructor, destructor) 
} 

當我創建一個實例,$z = new Derived;,那麼這首先構建Base子對象,那麼Derived(即$z->foo)的成員對象,最後的Derived執行構造函數。

因此,我期望在完全相反的順序發生的破壞序列:

  1. 執行Derived

  2. 破壞的Derived

  3. 成員對象執行Base析構函數。

然而,由於PHP不調用析構函數的基礎或基礎構造暗示,這是行不通的,我們必須使基地析構函數調用派生的析構函數中明確。但是,這破壞了現在「派生」,「基礎」,「成員」的破壞順序。

以下是我的擔心:如果任何成員對象要求基本子對象的狀態對其自身的操作有效,則這些成員對象在其自身的銷燬過程中不能依賴該基本子對象,因爲該基礎對象已經失效。

這是一個真正的問題,還是有語言中的某些東西阻止這種依賴發生?

這裏是在C++中的示例,其演示了正確的破壞序列的需要:

class ResourceController 
{ 
    Foo & resource; 
public: 
    ResourceController(Foo & rc) : resource(rc) { } 
    ~ResourceController() { resource.do_important_cleanup(); } 
}; 

class Base 
{ 
protected: 
    Foo important_resource; 
public: 
    Base() { important_resource.initialize(); } // constructor 
    ~Base() { important_resource.free(); }  // destructor 
} 

class Derived 
{ 
    ResourceController rc; 
public: 
    Derived() : Base(), rc(important_resource) { } 
    ~Derived() { } 
}; 

當我實例Derived x;,則基子對象首先構造,其設置important_resource。然後,成員對象rc被初始化爲參考important_resource,這在rc的銷燬期間是必需的。因此當x的生存期結束時,首先調用派生的析構函數(無所事事),然後rc被銷燬,執行其清理工作,並且只有,然後Base子對象銷燬,釋放important_resource

如果破壞發生的次序不正確,那麼rc的析構函數會訪問無效的引用。

1

如果在構造函數中拋出異常,對象永遠不會生存(對象的zval至少有一個引用計數,這是析構函數需要的),因此沒有任何具有析構函數的析構函數可以被稱爲。

現在我想知道:如何在PHP中的類層次結構中實現正確的範圍展開,以便在出現異常時正確銷燬子對象?

在你給的例子中,沒有什麼可以放鬆的。但是對於遊戲,讓我們假設,你知道基礎構造函數可以拋出一個懷疑,但在調用它之前,你需要初始化$this->foo

然後,您只需要通過一個(暫時),以提高「$this」引用計數,這需要(略)比__construct一個局部變量更多,讓下鋪的這出$foo本身:

class Der extends Base 
{ 
    public function __construct() 
    { 
    parent::__construct(); 
    $this->foo = new Foo; 
    $this->foo->__ref = $this; # <-- make base and Der __destructors active 
    print("Der const.\n"); 
    throw new Exception("foo"); // #1 
    unset($this->foo->__ref); # cleanup for prosperity 
    } 

結果:

Base const. 
Foo const. 
Der const. 
Der destr. 
Base destr. 
Foo destr. 

Demo

你自己想想,如果你需要這個功能,或者不。

要調用Foo析構函數被調用時的順序,請取消設置析構函數中的屬性,如this example demonstrates

編輯:由於您可以控制構建對象的時間,因此您可以控制對象何時被銷燬。執行以下命令:

Der const. 
Base const. 
Foo const. 
Foo destr. 
Base destr. 
Der destr. 

與做:C++和PHP之間

class Base 
{ 
    public function __construct() { print("Base const.\n"); } 
    public function __destruct() { print("Base destr.\n"); } 
} 

class Der extends Base 
{ 
    public function __construct() 
    { 
    print("Der const.\n"); 
    parent::__construct(); 
    $this->foo = new Foo; 
    $this->foo->__ref = $this; # <-- make Base and Def __destructors active 
    throw new Exception("foo"); 
    unset($this->foo->__ref); 
    } 
    public function __destruct() 
    { 
    unset($this->foo); 
    parent::__destruct(); 
    print("Der destr.\n"); 
    } 
    public $foo; 
} 

class Foo 
{ 
    public function __construct() { print("Foo const.\n"); } 
    public function __destruct() { print("Foo destr.\n"); } 
} 


try { 
    $x = new Der; 
} catch (Exception $e) { 
} 
+0

我不確定我買了你的第一句話:如果構造函數拋出,成員對象和基礎成員對象可能已經被初始化並且可能需要適當的銷燬。在我的PHP範例中,想象一下'Foo'具有非平凡的成員;並查看我的C++示例,瞭解如何在基礎成員上存在派生類的依賴關係。 –

+0

另外,我認爲你的例子其實更加破碎:如果構造函數拋出,沒有對象,所以析構函數絕對不應該運行。但是,應該運行* member * objects的析構函數,然後運行基礎析構函數。換句話說,如果'Der :: __ construct()'拋出,銷燬序列應該是'Base :: const,Foo :: const,Foo :: dest,Base :: dest'。 –

+0

@kerrek SB:給第一個評論:'Foo'的析構函數已經在你的案例中被調用過了,'Foo'應該自己處理,不是嗎?在我的第一個代碼示例中,Der析構函數以及Base destrcutor都被調用。如果你想改變順序,你可以在Der析構函數中控制它,但我沒有改變它。 – hakre

1

一個主要的區別是,在PHP中,基類的構造函數和析構函數不會自動被調用。這是在the PHP Manual page for Constructors and Destructors明確提到:

注意:父構造函數則不會暗中調用如果子類定義了一個構造函數。爲了運行父構造函數,需要在子構造函數中調用parent :: __ construct()

...

構造函數一樣,父析構函數不會被隱式發動機調用。爲了運行父析構函數,必須在析構函數體中明確調用parent :: __destruct()

因此,PHP將任務完全由程序員正確調用基類的構造函數和析構函數,並且在必要時總是由程序員負責調用基類構造函數和析構函數。

以上段落要點必要時。很少會有這樣一種情況,即未能調用析構函數會「泄漏資源」。請記住,在調用基類構造函數時創建的基實例的數據成員將自己變爲未引用,因此將調用每個成員的析構函數(如果存在)。嘗試一下這個代碼:

<?php 

class MyResource { 
    function __destruct() { 
     echo "MyResource::__destruct\n"; 
    } 
} 

class Base { 
    private $res; 

    function __construct() { 
     $this->res = new MyResource(); 
    } 
} 

class Derived extends Base { 
    function __construct() { 
     parent::__construct(); 
     throw new Exception(); 
    } 
} 

new Derived(); 

輸出示例:

 
MyResource::__destruct 

Fatal error: Uncaught exception 'Exception' in /t.php:20 
Stack trace: 
#0 /t.php(24): Derived->__construct() 
#1 {main} 
    thrown in /t.php on line 20 

http://codepad.org/nnLGoFk1

在這個例子中,Derived構造函數調用Base構造函數,它創建了一個新的MyResource實例。當Derived隨後在構造函數中拋出異常時,由Base構造函數創建的MyResource實例變爲未引用。最終,將調用析構函數MyResource

可能需要調用析構函數的一種場景是析構函數與另一個系統(如關係數據庫管理系統,緩存,消息傳遞系統等)進行交互的地方。如果必須調用析構函數,那麼可以封裝析構函數作爲一個單獨的目的是通過類層次結構的影響(如在上面MyResource的例子),或使用一個捕獲塊:

class Derived extends Base { 
    function __construct() { 
     parent::__construct(); 
     try { 
      // The rest of the constructor 
     } catch (Exception $ex) { 
      parent::__destruct(); 
      throw $ex; 
     } 
    } 

    function __destruct() { 
     parent::__destruct(); 
    } 
} 

編輯:爲了模擬清理局部變量和最導出的數據成員類,你需要有一個捕獲塊清理每個局部變量或數據成員初始化成功:

class Derived extends Base { 
    private $x; 
    private $y; 

    function __construct() { 
     parent::__construct(); 
     try { 
      $this->x = new Foo(); 
      try { 
       $this->y = new Bar(); 
       try { 
        // The rest of the constructor 
       } catch (Exception $ex) { 
        $this->y = NULL; 
        throw $ex; 
       } 
      } catch (Exception $ex) { 
       $thix->x = NULL; 
       throw $ex; 
      } 
     } catch (Exception $ex) { 
      parent::__destruct(); 
      throw $ex; 
     } 
    } 

    function __destruct() { 
     $this->y = NULL; 
     $this->x = NULL; 
     parent::__destruct(); 
    } 
} 

這是它是如何在Java中實現,也Java 7's try-with-resources statement之前。

+0

解決方法還應該執行任何本地清理,以便派生的成員有機會在調用基本析構函數之前清理*。但是,假設派生的構造函數try塊包含'$ this-> x = new Foo; $ this-> y = new Bar;',並且'Foo'和'Bar'的構造函數都可能拋出,那麼在發生異常時不知道清理誰。 –

+0

@KerrekSB:對。這就是C++如何做到的。如果你在PHP中需要這種行爲,那麼訣竅是爲每個成功構建的成員設置一個* catch *塊。看我的編輯。 –