Scope unwinding in PHP class constructors

痞子三分冷 提交于 2019-12-05 01:21:55

I would like to explain why PHP behaves this way and why it actually makes (some) sense.

In PHP an object is destroyed as soon as there are no more references to it. A reference can be removed in a multitude of ways, e.g. by unset()ing a variable, by leaving scope or as part of shutdown.

If you understood this, you can easily understand what happens here (I'll explain the case without the Exception first):

  1. PHP enters shutdown, thus all variable references are removed.
  2. When the reference created by $x (to the instance of Der) is removed the object is destroyed.
  3. The derived destructor is called, which calls the base destructor.
  4. Now the reference from $this->foo to the Foo instance is removed (as part of destroying the member fields.)
  5. There aren't any more references to Foo either, so it is destroyed too and the destructor is called.

Imagine this would not work this way and member fields would be destroyed before calling the destructor: You couldn't access them anymore in the destructor. I seriously doubt that there is such a behavior in C++.

In the Exception case you need to understand that for PHP there never really existed an instance of the class, as the constructor never returned. How can you destruct something that was never constructed?


How do I fix it?

You don't. The mere fact that you need a destructor probably is a sign of bad design. And the fact that the destruction order matters that much to you, is even more.

This is not an answer, but rather a more detailed explanation of the motivation for the question. I don't want to clutter the question itself with this somewhat tangential material.

Here is an explanation of how I would have expected the usual destruction sequence of a derived class with members. Suppose the class is this:

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

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

When I create an instance, $z = new Derived;, then this first constructs the Base subobject, then the member objects of Derived (namely $z->foo), and finally the constructor of Derived executes.

Therefore, I expected the destruction sequence to occur in the exact opposite order:

  1. execute Derived destructor

  2. destroy member objects of Derived

  3. execute Base destructor.

However, since PHP does not call base destructors or base constructors implicitly, this doesn't work, and we have to make the base destructor call explicit inside the derived destructor. But that upsets the destruction sequence, which is now "derived", "base", "members".

Here's my concern: If any of the member objects require the state of the base subobject to be valid for their own operation, then none of these member objects can rely on that base subobject during their own destruction, because that base object has already been invalidated.

Is this a genuine concern, or is there something in the language that prevents such dependencies from happening?

Here is an example in C++ which demonstrates the need for the correct destruction sequence:

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

When I instantiate Derived x;, then the base subobject is constructed first, which sets up important_resource. Then the member object rc is initialized with a reference to important_resource, which is required during rc's destruction. So when the lifetime of x ends, the derived destructor is called first (doing nothing), then rc is destroyed, doing its cleanup job, and only then is the Base subobject destroyed, releasing important_resource.

If the destruction had occurred out of order, then rc's destructor would have accessed an invalid reference.

If you throw an exception inside a constructor, the object never comes to live (the zval of the object has at least a reference count of one, that's needed for the destructor), therefore there is nothing that has a destructor which could be called.

Now I wonder: How do you implement correct scope unwinding in a class hierarchy in PHP, so that subobjects are properly destroyed in the event of an exception?

In the example you give, there is nothing to unwind. But for the game, let's assume, you know that the base constructor can throw an exeception, but you need to initialize $this->foo prior calling it.

You then only need to raise the refcount of "$this" by one (temporarily), this needs (a little) more than a local variable in __construct, let's bunk this out to $foo itself:

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
  }

Result:

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

Demo

Think for yourself if you need this feature or not.

To control the order when the Foo destructor is called, unset the property in the destructor, like this example demonstrates.

Edit: As you can control the time when objects are constructed, you can control when objects are destructed. The following order:

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

is done with:

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

One major difference between C++ and PHP is that in PHP, base class constructors and destructors are not called automatically. This is explicitly mentioned on the PHP Manual page for Constructors and Destructors:

Note: Parent constructors are not called implicitly if the child class defines a constructor. In order to run a parent constructor, a call to parent::__construct() within the child constructor is required.

...

Like constructors, parent destructors will not be called implicitly by the engine. In order to run a parent destructor, one would have to explicitly call parent::__destruct() in the destructor body.

PHP thus leaves the task of properly calling base class constructors and destructors entirely up to the programmer, and it is always the programmer's responsibility to call the base class constructor and destructor when necessary.

The key point in the above paragraph is when necessary. Rarely will there be a situation where failing to call a destructor will "leak a resource". Keep in mind that data members of the base instance, created when the base class constructor is called, will themselves become unreferenced, so a destructor (if one exists) for each of these members will be called. Try it out with this code:

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

Sample output:

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

In this example, the Derived constructor calls the Base constructor, which creates a new MyResource instance. When Derived subsequently throws an exception in the constructor, the MyResource instance created by the Base constructor becomes unreferenced. Eventually, the MyResource destructor will be called.

One scenario where it might be necessary to call a destructor is where the destructor interacts with another system, such as a relational DBMS, cache, messaging system, etc. If a destructor must be called, then you could either encapsulate the destructor as a separate object unaffected by class hierarchies (as in the example above with MyResource) or use a catch block:

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: To emulate cleaning up local variables and data members of the most derived class, you need to have a catch block to clean up each local variable or data member that is successfully initialized:

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

This is how it was done in Java, too, before Java 7's try-with-resources statement.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!