10

私は PHP のクラスと例外を学んでいますが、C++ のバックグラウンドを持っているため、次のことは奇妙に思えます。

派生クラスのコンストラクターが例外をスローすると、基本クラスのデストラクターが自動的に実行されないように見えます。

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.

一方、メンバー オブジェクトのデストラクタは、コンストラクタ (at ) に例外がある場合は適切#1実行されます。ここで疑問に思うのは、例外が発生した場合にサブオブジェクトが適切に破棄されるように、PHP のクラス階層で正しいスコープの巻き戻しを実装するにはどうすればよいかということです。

また、すべてのメンバー オブジェクトが破棄された(で) 、基本デストラクタを実行する方法がないよう#2です。つまり、行を削除すると#1、次のようになります。

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

その問題をどのように解決しますか?

更新:私はまださらなる貢献を受け付けています。PHPオブジェクトシステムが正しい破棄シーケンスを必要としない理由を誰かが十分に正当化している場合、私はそのために別の報奨金を与えます (または、説得力のある他の答えのために)。

4

4 に答える 4

6

PHP がこのように動作する理由と、それが実際に (ある程度) 理にかなっている理由を説明したいと思います。

PHP では、オブジェクトへの参照がなくなるとすぐにオブジェクトが破棄されます。参照は、さまざまな方法で削除できます。たとえばunset()、変数を ing したり、スコープを離れたり、シャットダウンの一部として削除したりできます。

これを理解していれば、ここで何が起こるかを簡単に理解できます (最初に例外がない場合について説明します)。

  1. PHP がシャットダウンに入り、すべての変数参照が削除されます。
  2. によって作成された$x( のインスタンスへのDer) 参照が削除されると、オブジェクトは破棄されます。
  3. 派生デストラクタが呼び出され、ベース デストラクタが呼び出されます。
  4. $this->fooからインスタンスへの参照Fooが削除されます (メンバー フィールドの破棄の一部として)。
  5. どちらへの参照もこれ以上ないFooので、それも破棄され、デストラクタが呼び出されます。

これがこのように機能せず、デストラクタを呼び出す前にメンバー フィールドが破棄されると想像してください。デストラクタでそれらにアクセスできなくなります。C++ にそのような動作があるとは思えません。

Exception のケースでは、PHP の場合、コンストラクターが返されないため、実際にはクラスのインスタンスが存在しないことを理解する必要があります。構築されたことのないものをどのように破壊できますか?


どうすれば修正できますか?

あなたはそうしない。デストラクタが必要であるという単なる事実は、おそらく悪い設計の兆候です。そして、破壊順序があなたにとってそれほど重要であるという事実は、さらに重要です.

于 2011-09-23T06:42:55.310 に答える
2

これは答えではなく、質問の動機のより詳細な説明です。このやや接線的な資料で質問自体を混乱させたくありません。

これは、メンバーを持つ派生クラスの通常の破棄シーケンスをどのように期待したかについての説明です。クラスがこれであるとします:

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

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

インスタンスを作成すると$z = new Derived;、これは最初にサブオブジェクトを作成し、次に(つまり)Baseのメンバ オブジェクトを作成し、最後に のコンストラクタを作成します。Derived$z->fooDerived

したがって、破壊シーケンスが正反対の順序で発生することを期待していました。

  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のデストラクタは無効な参照にアクセスしていたでしょう。

于 2011-09-23T13:48:17.457 に答える
1

C++ と PHP の大きな違いの 1 つは、PHP では基底クラスのコンストラクターとデストラクターが自動的に呼び出されないことです。これは、 PHP マニュアルの 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

致命的なエラー: /t.php:20 で例外 'Exception' がキャッチされていません
スタックトレース:
#0 /t.php(24): 派生->__construct()
#1 {メイン}
  20 行目の /t.php にスローされます

http://codepad.org/nnLGoFk1

この例では、Derivedコンストラクターがコンストラクターを呼び出し、Base新しいMyResourceインスタンスを作成します。Derivedその後、コンストラクターで例外をスローすると、コンストラクターによって作成されたインスタンスMyResourceBase参照されなくなります。最終的に、MyResourceデストラクタが呼び出されます。

デストラクタを呼び出す必要があるシナリオの 1 つは、デストラクタがリレーショナル DBMS、キャッシュ、メッセージング システムなどの別のシステムと対話する場合です。デストラクタを呼び出す必要がある場合は、デストラクタを別のシステムとしてカプセル化できます。クラス階層の影響を受けないオブジェクト (上記の の例のようにMyResource) またはcatchブロックを使用します。

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

編集:最も派生したクラスのローカル変数とデータ メンバーのクリーンアップをエミュレートするには、正常に初期化された各ローカル変数またはデータ メンバーをクリーンアップするためのcatchブロックが必要です。

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 7 の try-with-resources ステートメントの前に、Java でも行われていた方法です。

于 2011-09-29T13:21:15.057 に答える
1

コンストラクター内で例外をスローすると、オブジェクトが有効になることはありません (オブジェクトの zval には、デストラクターに必要な少なくとも 1 つの参照カウントがあります)。したがって、呼び出すことができるデストラクターを持つものはありません。

ここで疑問に思うのは、例外が発生した場合にサブオブジェクトが適切に破棄されるように、PHP のクラス階層で正しいスコープの巻き戻しを実装するにはどうすればよいかということです。

あなたが与える例では、ほどけるものは何もありません。しかし、ゲームの場合、基本コンストラクターが例外をスローできることはわかっていますが、それを呼び出す前に初期化する必要があると仮定しましょう$this->foo

次に、 " " の refcount を (一時的に) 1 つ上げるだけで済みます$this。これには、ローカル変数 in よりも (少し) 多く必要です。これを自分自身にまとめ__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.

デモ

この機能が必要かどうかは自分で考えてください。

Foo デストラクタが呼び出される順序を制御するには、この例が示すように、デストラクタでプロパティの設定を解除します。

編集:オブジェクトが構築される時間を制御できるように、オブジェクトが破壊される時間を制御できます。次の順序:

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

で行われます:

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) {
}
于 2011-09-28T20:30:45.473 に答える