PHP 类构造函数中的范围展开

发布于 2024-12-05 10:42:48 字数 1294 浏览 5 评论 0原文

我正在学习 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) {
}

这prints:

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 对象系统从不需要正确的销毁序列,我将为此提供另一个赏金(或只是为了任何其他令人信服的争论答案)。

I'm learning PHP classes and exceptions, and, coming from a C++ background, the following strikes me as odd:

When the constructor of a derived class throws an exception, it appears that the destructor of the base class is not run automatically:

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

This prints:

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

On the other hand, the destructors of member objects are executed properly if there is an exception in the constructor (at #1). 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?

Also, it seems that there's no way to run the base destructor after all the member objects have been destroyed (at #2). To wit, if we remove line #1, we get:

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

How would one solve that problem?

Update: I'm still open to further contributions. If someone has a good justification why the PHP object system never requires a correct destruction sequence, I'll give out another bounty for that (or just for any other convincingly argued answer).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

ゃ人海孤独症 2024-12-12 10:42:48

我想解释为什么 PHP 会这样做,以及为什么它实际上(在某种程度上)有意义。

在 PHP 中,一旦不再有对象的引用,该对象就会被销毁。可以通过多种方式删除引用,例如通过 unset() 变量、离开作用域或作为关闭的一部分。

如果您理解了这一点,您就可以轻松理解这里发生的情况(我将首先解释没有异常的情况):

  1. PHP 进入关闭状态,因此所有变量引用都被删除。
  2. $x 创建的引用(对 Der 的实例)被删除时,该对象将被销毁。
  3. 调用派生析构函数,派生析构函数又调用基析构函数。
  4. 现在,从 $this->fooFoo 实例的引用已被删除(作为销毁成员字段的一部分。)
  5. 不再有对 的引用>Foo 也一样,所以它也被销毁并调用析构函数。

想象一下,这种方式行不通,并且成员字段将在调用析构函数之前被销毁:您无法再在析构函数中访问它们。我严重怀疑C++中是否存在这种行为。

在异常情况下,您需要了解对于 PHP 来说,从来没有真正存在过该类的实例,因为构造函数从未返回。你怎么能破坏那些从未建造过的东西呢?


我该如何修复它?

你不知道。您需要析构函数这一事实可能就是糟糕设计的标志。事实上,销毁命令对你来说非常重要,这一点甚至更重要。

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.

北城挽邺 2024-12-12 10:42:48

这不是答案,而是对问题动机的更详细解释。我不想用这种有点离题的材料来混淆问题本身。

这是我如何预期具有成员的派生类的通常破坏顺序的解释。假设类是这样的:

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. 销毁<的成员对象code>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。然后,使用对 important_resource 的引用来初始化成员对象 rc,这是在 rc 销毁期间需要的。因此,当x的生​​命周期结束时,首先调用派生的析构函数(不执行任何操作),然后rc被销毁,执行其清理工作,然后然后< /em> 是被销毁的Base 子对象,释放important_resource

如果破坏发生无序,则 rc 的析构函数将访问无效引用。

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.

烟燃烟灭 2024-12-12 10:42:48

如果在构造函数内抛出异常,则该对象永远不会存在(该对象的 zval 的引用计数至少为 1,这是析构函数所需的),因此没有任何东西可以调用析构函数。

现在我想知道:如何在 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.

演示

自己思考是否需要此功能或不是。

要控制调用 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) {
}

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) {
}
浪漫人生路 2024-12-12 10:42:48

C++ 和 PHP 之间的一个主要区别是,在 PHP 中,不会自动调用基类构造函数和析构函数。 构造函数和析构函数的 PHP 手册页面明确提到了这一点

注意:如果子类定义了构造函数,则不会隐式调用父构造函数。为了运行父构造函数,需要在子构造函数中调用 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 析构函数将被调用。

可能需要调用析构函数的一种场景是析构函数与另一个系统交互,例如关系 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中的做法,之前Java 7 的 try-with-resources声明

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文