2011-09-19 37 views
10

tôi đang học lớp PHP và trường hợp ngoại lệ, và, đến từ một C++ nền, sau cuộc đình tôi như lẻ:Phạm vi ươm trong constructor của lớp cha PHP

Khi các nhà xây dựng của một lớp có nguồn gốc ném một ngoại lệ, nó xuất hiện rằng destructor của lớp cơ sở không được chạy tự động:

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) { 
} 

này in:

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

Mặt khác, các destructors của các đối tượng thành viên được thực thi đúng cách nếu có ngoại lệ trong hàm tạo (tại #1). Bây giờ tôi tự hỏi: Làm thế nào để bạn thực hiện đúng phạm vi thư giãn trong một hệ thống phân cấp lớp trong PHP, để subobjects bị phá hủy đúng trong trường hợp ngoại lệ?

Ngoài ra, có vẻ như không có cách nào để chạy trình phá hủy cơ sở sau tất cả các đối tượng thành viên đã bị hủy (tại #2). Để wit, nếu chúng ta loại bỏ dòng #1, chúng tôi nhận được:

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

Làm thế nào người ta sẽ giải quyết vấn đề đó?

Cập nhật: Tôi vẫn mở để đóng góp thêm. Nếu ai đó có lý do chính đáng tại sao hệ thống đối tượng PHP không bao giờ yêu cầu một chuỗi hủy diệt chính xác, tôi sẽ đưa ra một tiền thưởng khác cho điều đó (hoặc chỉ cho bất kỳ câu trả lời thuyết phục khác).

+1

Tôi phải nói rằng tôi rất rất hiếm khi cần thiết để thực hiện các destructors trong PHP, vì vậy có lẽ nó không phải là một vấn đề. Bạn có nêu ra một câu hỏi hay. –

+0

@Jani: Thành thật mà nói, với cách chúng được thiết kế, tôi hiểu tại sao bạn thực sự không muốn * muốn * sử dụng các destructors. Tôi chỉ tự hỏi tại sao chúng lại bị suy nghĩ rất tệ, và liệu có thành ngữ chung nào để phá vỡ lỗ hổng thiết kế này ngoài "không sử dụng phần này của ngôn ngữ" ...: -S –

+1

Đồng ý với Jani: trong PHP thực sự không có điểm nào trong việc viết các destructors vì không có gì bạn có thể bị rò rỉ. Đến từ C++, có thể bạn đang xem các trình phá hủy như một công cụ rất tồi tệ để giải quyết một vấn đề mà họ không có ý định giải quyết. – Jon

Trả lời

6

Tôi muốn giải thích lý do tại sao PHP hoạt động theo cách này và tại sao nó thực sự làm cho (một số) ý nghĩa.

Trong PHP một đối tượng bị hủy ngay sau khi không có thêm tham chiếu đến nó. Có thể xóa tham chiếu bằng nhiều cách, ví dụ: bằng cách unset() nhập một biến, bằng cách rời khỏi phạm vi hoặc như một phần của tắt máy.

Nếu bạn hiểu điều này, bạn có thể dễ dàng hiểu được những gì xảy ra ở đây (tôi sẽ giải thích các trường hợp mà không có ngoại lệ đầu tiên):

  1. PHP vào tắt máy, do đó tất cả các tài liệu tham khảo biến được loại bỏ.
  2. Khi tham chiếu được tạo bởi $x (đối với trường hợp Der) bị xóa, đối tượng sẽ bị hủy.
  3. Các destructor có nguồn gốc được gọi là, mà gọi destructor cơ sở.
  4. Bây giờ tài liệu tham khảo từ $this->foo đến Foo dụ được lấy ra (như một phần của phá hủy các trường thành viên.)
  5. Không có nhiều bất kỳ tài liệu tham khảo để Foo một trong hai, vì vậy nó bị phá hủy quá và destructor được gọi.

Hãy tưởng tượng điều này sẽ không hoạt động theo cách này và các trường thành viên sẽ bị hủy trước khi gọi hàm hủy: Bạn không thể truy cập chúng nữa trong trình hủy. Tôi nghiêm túc nghi ngờ rằng có một hành vi như vậy trong C + +.

Trong trường hợp ngoại lệ, bạn cần hiểu rằng đối với PHP, không bao giờ thực sự tồn tại một cá thể của lớp, vì hàm tạo không bao giờ trả về. Làm thế nào bạn có thể phá hủy một cái gì đó mà không bao giờ được xây dựng?


Làm cách nào để khắc phục sự cố?

Bạn không. Thực tế là bạn cần một destructor có lẽ là một dấu hiệu của thiết kế xấu. Và thực tế là trật tự hủy diệt quan trọng đối với bạn, thậm chí còn nhiều hơn thế.

+0

Tôi không chắc tôi mua lời giải thích này: Khi tôi nói 'ouch' trong ví dụ, đó là bởi vì tôi mong đợi chuỗi hủy diệt được "bắt nguồn - foo - base"; nhưng tất nhiên điều đó không xảy ra bởi vì tôi thực sự gọi destructor cơ bản một cách rõ ràng. Nhưng hãy tưởng tượng đối tượng '$ this-> foo' này phụ thuộc vào trạng thái hợp lệ của subobject' Base'. Bây giờ sự phá hủy của '$ this-> foo' có thể cần thực hiện một số shutdown mà yêu cầu subobject' Base', nhưng nó không còn hợp lệ nữa. Có một lý do tại sao điều này không thể hoặc không nên xảy ra? –

+0

@KerrekSB Tôi đã giải thích rằng các thành viên cần phải bị phá hủy sau khi destructor được gọi, nếu không bạn không thể truy cập chúng trong destructor. Điều này cũng giống như trong C++ (afaik). Hơn nữa: $ this-> foo * không được * phụ thuộc vào đối tượng 'Base' (nó nên thực sự truy cập nó như thế nào?). '$ this-> foo' là một phụ thuộc (nên được tiêm, xem DI, IoC và SOLID). Nó thậm chí không nên biết rằng nó được sử dụng trong lớp khác và chắc chắn không dựa vào đó. – NikiC

+0

Sự khác biệt giữa cơ sở và đối tượng dẫn xuất là quan trọng. Thật vậy, các destructor có nguồn gốc đã đến đầu tiên. Nhưng '$ this-> foo' được xây dựng * sau khi * subobject cơ bản được xây dựng, nên nó sẽ bị phá hủy * trước * base, phải không? Tôi sẽ thêm một ví dụ vào bài đăng! –

2

Đây không phải là câu trả lời, mà là giải thích chi tiết hơn về động lực cho câu hỏi. Tôi không muốn làm lộn xộn câu hỏi với tài liệu có phần tiếp tuyến này.

Dưới đây là giải thích về cách tôi có thể mong đợi chuỗi hủy diệt thông thường của lớp có nguồn gốc với các thành viên. Giả sử lớp là thế này:

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

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

Khi tôi tạo ra một thể hiện, $z = new Derived;, thì đây đầu tiên xây dựng các Base subobject, sau đó các đối tượng thành viên của Derived (cụ thể là $z->foo), và cuối cùng các nhà xây dựng của Derived thực thi.

Vì vậy, tôi mong đợi chuỗi hủy diệt xảy ra theo thứ tự ngược lại:

  1. thực hiện Derived destructor

  2. phá hủy đối tượng thành viên của Derived

  3. thực hiện Base destructor.

Tuy nhiên, vì PHP không gọi hàm hủy cơ sở hoặc hàm tạo cơ sở ngầm rõ ràng, điều này không hoạt động và chúng tôi phải thực hiện lệnh gọi phá hủy cơ sở rõ ràng bên trong trình dẫn xuất có nguồn gốc. Nhưng điều đó làm rối loạn trình tự hủy diệt, mà bây giờ là "có nguồn gốc", "cơ sở", "các thành viên". Đây là mối quan tâm của tôi: Nếu bất kỳ đối tượng thành viên nào yêu cầu trạng thái của subobject cơ sở hợp lệ cho hoạt động riêng của chúng, thì không có đối tượng nào trong số này có thể dựa vào subobject cơ sở đó trong quá trình hủy của chúng. đã bị vô hiệu.

Đây có phải là mối quan tâm thực sự hoặc có điều gì đó trong ngôn ngữ ngăn chặn sự phụ thuộc như vậy không?

Dưới đây là một ví dụ trong C++ mà chứng minh sự cần thiết của chuỗi hủy diệt chính xá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() { } 
}; 

Khi tôi nhanh chóng Derived x; thì subobject cơ sở được xây dựng đầu tiên, trong đó thiết lập important_resource. Sau đó, đối tượng thành viên rc được khởi tạo với tham chiếu đến important_resource, được yêu cầu trong khi hủy diệt rc.Vì vậy, khi tuổi thọ của x kết thúc, hàm hủy có nguồn gốc được gọi là đầu tiên (không làm gì), sau đó rc bị hủy, thực hiện công việc dọn dẹp và chỉ sau đó là bị hủy Base subobject, phát hành important_resource.

Nếu sự phá hủy xảy ra không đúng thứ tự, sau đó phá hủy rc sẽ truy cập tham chiếu không hợp lệ.

1

Nếu bạn ném một ngoại lệ bên trong một hàm tạo, đối tượng không bao giờ xuất hiện (giá trị của đối tượng có ít nhất một số tham chiếu của đối tượng), do đó không có gì có hàm hủy có thể được gọi.

Bây giờ tôi tự hỏi: Làm thế nào để bạn triển khai đúng phạm vi giải quyết trong một hệ thống phân cấp lớp trong PHP, để các subobject bị phá hủy đúng trong trường hợp ngoại lệ?

Trong ví dụ bạn đưa ra, không có gì để thư giãn. Nhưng đối với trò chơi, giả sử, bạn biết rằng hàm tạo cơ sở có thể ném một hành vi lừa đảo, nhưng bạn cần phải khởi tạo $this->foo trước khi gọi nó.

Sau đó, bạn chỉ cần để nâng cao refcount của "$this" bởi một (tạm thời), điều này cần (một chút) hơn một biến địa phương trong __construct, chúng ta hãy bunk này ra để $foo bản thân:

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 
    } 

Kết quả:

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

Demo

suy nghĩ cho chính mình nếu bạn cần tính năng này hay không.

Để kiểm soát thứ tự khi trình phá hủy Foo được gọi, hãy hủy đặt thuộc tính trong trình phá hủy, như this example demonstrates.

Chỉnh sửa: Khi bạn có thể kiểm soát thời gian khi đối tượng được tạo, bạn có thể kiểm soát khi đối tượng bị hủy. Trình tự sau:

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

được thực hiện với:

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

Tôi không chắc chắn tôi mua câu đầu tiên của bạn: Nếu một nhà xây dựng ném, các đối tượng thành viên và các đối tượng thành viên cơ sở có thể đã được khởi tạo và có thể yêu cầu hủy diệt thích hợp. Trong exampe PHP của tôi, hãy tưởng tượng rằng 'Foo' có những thành viên không tầm thường; và xem ví dụ C++ của tôi về cách có thể có sự phụ thuộc của lớp dẫn xuất trên các thành viên cơ sở. –

+0

Ngoài ra, tôi nghĩ rằng ví dụ của bạn là thực sự thậm chí nhiều hơn bị hỏng: Nếu các nhà xây dựng ném, không có đối tượng, do đó, destructor chắc chắn * không * chạy. Tuy nhiên, các destructor của * member * objects 'nên chạy, tiếp theo là destructor cơ bản. Nói cách khác, nếu 'Der :: __ construct()' ném, chuỗi hủy diệt phải là 'Base :: const, Foo :: const, Foo :: dest, Base :: dest'. –

+0

@kerrek SB: Để bình luận đầu tiên: Các destructor của 'Foo' được gọi là trong trường hợp của bạn đã có,' Foo' nên chăm sóc bản thân, phải không? Trong ví dụ đầu tiên của tôi, Der destructor được gọi cũng như Base destrcutor. Nếu bạn muốn thay đổi thứ tự, bạn có quyền kiểm soát điều đó trong Der destructor, tôi đã không thay đổi nó. – hakre

1

Một khác biệt lớn giữa C++ và PHP là trong PHP, constructor của lớp cha và destructor không được gọi tự động. Này được đề cập một cách rõ ràng trên the PHP Manual page for Constructors and Destructors:

Note: nhà xây dựng Chánh không được gọi ngầm nếu lớp con định nghĩa một constructor. Để chạy hàm khởi tạo gốc, hãy gọi đến số cấp độ gốc :: __ construct() trong hàm tạo con.

...

Giống như các nhà thầu, trình hủy phụ huynh sẽ không được gọi ngầm bởi động cơ. Để chạy trình phá hoại gốc, người dùng phải gọi một cách rõ ràng parent :: __ destruct() trong phần tử hủy.

PHP do đó để nhiệm vụ gọi đúng các nhà xây dựng lớp cơ sở và trình phá hủy hoàn toàn cho người lập trình và nó luôn là trách nhiệm của người lập trình gọi hàm tạo và lớp hủy cơ sở khi cần thiết.

Điểm chính trong đoạn trên là khi cần thiết. Hiếm khi sẽ có một tình huống mà không gọi một destructor sẽ "rò rỉ một nguồn tài nguyên". Hãy nhớ rằng các thành viên dữ liệu của cá thể cơ sở, được tạo khi hàm tạo của lớp cơ sở được gọi, sẽ tự động trở thành không bị cản trở, do đó một destructor (nếu có) cho mỗi thành viên này sẽ được gọi. Hãy thử nó với mã này: đầu ra

<?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(); 

mẫu:

 
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

Trong ví dụ này, các nhà xây dựng Derived gọi Base constructor, mà tạo ra một MyResource dụ mới. Khi Derived sau đó ném một ngoại lệ trong hàm tạo, ví dụ MyResource được tạo bởi hàm tạo Base trở nên không được chấp nhận. Cuối cùng, các destructor MyResource sẽ được gọi. Một tình huống mà nó có thể cần thiết để gọi một destructor là nơi mà destructor tương tác với một hệ thống khác, chẳng hạn như một DBMS quan hệ, bộ nhớ đệm, hệ thống nhắn tin, vv Nếu một destructor phải được gọi, sau đó bạn có thể đóng gói destructor như một đối tượng riêng biệt không bị ảnh hưởng bởi phân cấp lớp (như trong ví dụ trên với MyResource) hoặc sử dụng một bắt khối:

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(); 
    } 
} 

EDIT: Để thi đua dọn dẹp các biến địa phương và các thành viên dữ liệu của các nguồn gốc nhất lớp học, bạn cần có bắt khối để làm sạch mỗi biến hoặc dữ liệu thành viên địa phương được khởi tạo thành công:

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(); 
    } 
} 

Đây là cách nó đã được thực hiện trong Java cũng vậy, trước khi Java 7's try-with-resources statement.

+0

Cách giải quyết cũng nên thực hiện bất kỳ việc dọn dẹp cục bộ nào, để các thành viên của 'Derived' có cơ hội dọn dẹp * trước * trình phá hủy cơ sở được gọi. Tuy nhiên, giả sử khối khởi tạo của hàm khởi tạo có chứa '$ this-> x = new Foo; $ this-> y = new Bar; ', và cả hai' Foo' và 'Bar' của constructor có thể ném, sau đó bạn không biết ai để làm sạch trong trường hợp ngoại lệ. –

+0

@KerrekSB: Đúng vậy. Đó là cách C++ làm điều đó. Nếu bạn cần hành vi đó trong PHP, thì thủ thuật là có một khối * catch * cho mọi thành viên được xây dựng thành công. Xem chỉnh sửa của tôi. –

Các vấn đề liên quan