PHP 类构造函数中的范围展开
我正在学习 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我想解释为什么 PHP 会这样做,以及为什么它实际上(在某种程度上)有意义。
在 PHP 中,一旦不再有对象的引用,该对象就会被销毁。可以通过多种方式删除引用,例如通过 unset() 变量、离开作用域或作为关闭的一部分。
如果您理解了这一点,您就可以轻松理解这里发生的情况(我将首先解释没有异常的情况):
$x
创建的引用(对Der
的实例)被删除时,该对象将被销毁。$this->foo
到Foo
实例的引用已被删除(作为销毁成员字段的一部分。)的引用>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):
$x
(to the instance ofDer
) is removed the object is destroyed.$this->foo
to theFoo
instance is removed (as part of destroying the member fields.)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.
这不是答案,而是对问题动机的更详细解释。我不想用这种有点离题的材料来混淆问题本身。
这是我如何预期具有成员的派生类的通常破坏顺序的解释。假设类是这样的:
当我创建一个实例时,
$z = new Derived;
,那么它首先构造Base
子对象,然后构造Derived的成员对象
(即$z->foo
),最后执行Derived
的构造函数。因此,我期望销毁顺序以完全相反的顺序发生:
执行
Derived
析构函数销毁<的成员对象code>Derived
执行
Base
析构函数。但是,由于 PHP 不会隐式调用基析构函数或基构造函数,因此这不起作用,我们必须在派生析构函数内显式调用基析构函数。但这打乱了破坏顺序,现在是“派生”、“基础”、“成员”。
我担心的是:如果任何成员对象要求基子对象的状态对于它们自己的操作有效,那么这些成员对象在它们自己的销毁过程中都不能依赖该基子对象,因为该基对象已经失效。
这是真正的担忧吗,还是语言中存在某种东西可以防止这种依赖关系的发生?
下面是一个 C++ 示例,演示了正确销毁顺序的必要性:
当我实例化
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:
When I create an instance,
$z = new Derived;
, then this first constructs theBase
subobject, then the member objects ofDerived
(namely$z->foo
), and finally the constructor ofDerived
executes.Therefore, I expected the destruction sequence to occur in the exact opposite order:
execute
Derived
destructordestroy member objects of
Derived
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:
When I instantiate
Derived x;
, then the base subobject is constructed first, which sets upimportant_resource
. Then the member objectrc
is initialized with a reference toimportant_resource
, which is required duringrc
's destruction. So when the lifetime ofx
ends, the derived destructor is called first (doing nothing), thenrc
is destroyed, doing its cleanup job, and only then is theBase
subobject destroyed, releasingimportant_resource
.If the destruction had occurred out of order, then
rc
's destructor would have accessed an invalid reference.如果在构造函数内抛出异常,则该对象永远不会存在(该对象的 zval 的引用计数至少为 1,这是析构函数所需的),因此没有任何东西可以调用析构函数。
在你给出的例子中,没有什么可以放松的。但对于游戏,我们假设,您知道基本构造函数可以抛出异常,但您需要在调用它之前初始化
$this->foo
。然后,您只需将“
$this
”的引用计数提高一(暂时),这需要(一点)多于__construct
中的局部变量,让我们把它扔掉到$foo
本身:结果:
演示
自己思考是否需要此功能或不是。
要控制调用 Foo 析构函数时的顺序,请取消设置析构函数中的属性,如此示例演示。
编辑:正如您可以控制对象构造的时间一样,您也可以控制对象销毁的时间。以下顺序:
完成:
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.
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:Result:
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:
is done with:
C++ 和 PHP 之间的一个主要区别是,在 PHP 中,不会自动调用基类构造函数和析构函数。 构造函数和析构函数的 PHP 手册页面明确提到了这一点:
因此,PHP 将正确调用基类构造函数和析构函数的任务完全交给程序员,并且在必要时调用基类构造函数和析构函数始终是程序员的责任。
上一段的重点是必要时。很少会出现未能调用析构函数而导致“资源泄漏”的情况。请记住,在调用基类构造函数时创建的基实例的数据成员本身将变为未引用,因此将调用每个成员的析构函数(如果存在)。使用以下代码尝试一下:
示例输出:
http://codepad.org/nnLGoFk1
在此示例中,
Derived
构造函数调用Base
构造函数,后者创建一个新的MyResource
实例。当Derived
随后在构造函数中引发异常时,由Base
构造函数创建的MyResource
实例将变为未引用。最终,MyResource
析构函数将被调用。可能需要调用析构函数的一种场景是析构函数与另一个系统交互,例如关系 DBMS、缓存、消息传递系统等。如果必须调用析构函数,那么您可以将析构函数封装为单独的析构函数。不受类层次结构影响的对象(如上面的
MyResource
示例)或使用 catch 块:编辑: 模拟清理本地最底层派生类的变量和数据成员,需要有一个catch块来清理成功初始化的每个局部变量或数据成员:
这也是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:
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:
Sample output:
http://codepad.org/nnLGoFk1
In this example, the
Derived
constructor calls theBase
constructor, which creates a newMyResource
instance. WhenDerived
subsequently throws an exception in the constructor, theMyResource
instance created by theBase
constructor becomes unreferenced. Eventually, theMyResource
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: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:
This is how it was done in Java, too, before Java 7's try-with-resources statement.