【问题标题】:php destructor called too soon with fluent interfacephp析构函数调用过快,界面流畅
【发布时间】:2013-04-16 08:10:49
【问题描述】:

我发现了一个关于 php 析构函数的很奇怪的事情:

基本上我有一个数据库管理类,它使用工厂加载适配器来定义应该加载哪个适配器(mysql、mysqli 等)

我将只写下有趣的部分代码,因为类本身要长得多,但当前的问题不涉及代码

这个问题只发生在 mysql 上(mysqli 和 pdo 工作得很好),但出于兼容性目的,摆脱 mysql 是不可能的。

class manager
{
    private static $_instance;

    public static function getInstance()
    {
        return isset(self::$_instance) ? self::$_instance : self::$_instance = new self;
    }

    public function getStandaloneAdapter()
    {
        return new mysql_adapter(array('host'=>'127.0.0.1', 'username' => 'root', 'password' => '', 'dbname' => 'etab_21'));
    }
}

abstract class abstract_adapter
{
    protected $_params;
    protected $_connection;

    public function __construct($params)
    {
        $this->_params = (object)$params;
    }

    public function __destruct()
    {
        echo 'destructor<br/>';
        var_dump(debug_backtrace(false));
        $this->closeConnection();
    }

    abstract public function closeConnection();
}

class mysql_adapter extends abstract_adapter
{
    public function getConnection()
    {
        $this->_connect();

        if ($this->_connection) {
            // switch database
            $this->_useDB($this->_params->dbname);
        }

        return $this->_connection;
    }

    protected function _connect()
    {
        if ($this->_connection) {
            return;
        }

        // connect
        $this->_connection = mysql_connect(
            $this->_params->host,
            $this->_params->username,
            $this->_params->password,
            true
        );

        if (false === $this->_connection || mysql_errno($this->_connection)) {
            $this->closeConnection();
            throw new Mv_Core_Db_Exception(null, Mv_Core_Db_Exception::CONNECT, mysql_error());
        }

        if ($this->_params->dbname) {
            $this->_useDB($this->_params->dbname);
        }
    }

    private function _useDB($dbname)
    {
        return mysql_select_db($dbname, $this->_connection);
    }

    public function isConnected()
    {
        $isConnected = false;
        if (is_resource($this->_connection)) {
            $isConnected = mysql_ping($this->_connection);
        }
        return $isConnected;
    }

    public function closeConnection()
    {
        if ($this->isConnected()) {
            mysql_close($this->_connection);
        }
        $this->_connection = null;
    }
}

这是我正在运行的测试:

$sadb1 = manager::getInstance()->getStandaloneAdapter()->getConnection();
var_dump($sadb1);

我得到的输出:

destructor
array
  0 => 
    array
      'file' => string '**\index.php' (length=48)
      'line' => int 119
      'function' => string '__destruct' (length=10)
      'class' => string 'abstract_adapter' (length=16)
      'type' => string '->' (length=2)
      'args' => 
        array
          empty
  1 => 
    array
      'file' => string '**\index.php' (length=48)
      'line' => int 119
      'function' => string 'unknown' (length=7)
resource(26, Unknown)

如果我把我的测试改成这样:

$sadb1 = manager::getInstance()->getStandaloneAdapter();
var_dump($sadb1->getConnection());

输出不错:

resource(26, mysql link)
destructor
array
  0 => 
    array
      'function' => string '__destruct' (length=10)
      'class' => string 'abstract_adapter' (length=16)
      'type' => string '->' (length=2)
      'args' => 
        array
          empty

wtf?!

【问题讨论】:

  • 出于好奇,为什么factory函数的第一行是:return new Mv_Core_Db_Adapter_Mysql...?使函数的其余部分变得毫无意义。
  • 我已经编辑了我的代码,我已经删除了所有不必要的行并保持最少,仍然无法正常工作 x_x
  • 在您的测试期间,除了课程之外,只有这两行用于测试吗?在使用mysql 版本时,您说mysqlipdo 工作正常,在您的代码流中,使用mysql 有什么区别?这是一个不寻常的情况,因此在其余代码中的可用性有限?
  • 由于您从我第一次看到它到您的实际问题大幅削减了代码,+1 为研究工作和一个我以前从未见过的关于垃圾收集过程的好问题. ^^

标签: php mysql destructor fluent-interface


【解决方案1】:

在第一个测试中运行的早期析构函数是自动垃圾收集的结果。为了理解这一点,让我们看一下第二个(更简单的)测试:

1. $db = Mv_Core_Db_Manager::getInstance();
2. $sadb = $db->getStandaloneAdapter('bdm_bcb');
3. var_dump($sadb->getConnection());

步骤:

  1. 正在将 db 管理器分配给变量 $db,
  2. 独立适配器(本例中的 MySQL 适配器是由于您为调试而引入的工厂中的 hack)被分配给 $sadb,
  3. var_dump() 调试 $sadb 独立适配器的 getConnection() 方法的返回值,即第二个输出中的 resource(29, mysql link) 行,
  4. 清理时间; PHP 垃圾收集器运行 $sadb 的析构函数(由于调试,在您的输出中可见),然后是 $db(在您的输出中不可见)。

垃圾回收在最后发生。

如果您考虑您描述的第一个测试,尽管源代码看起来很相似,但它有不同的步骤:

1. $db = Mv_Core_Db_Manager::getInstance();
2. $sadb = $db->getStandaloneAdapter('bdm_bcb')->getConnection();
3. var_dump($sadb);

步骤:

  1. 与上面的测试用例相同,
  2. MySQL 独立适配器对象的getConnection() getter 的返回值分配给 $sadb,
  3. 因为 MySQL 独立适配器本身没有分配给任何变量,PHP 垃圾收集器决定不再使用它,因此它清理对象并运行其析构函数(析构函数调试在你的输出首先),
  4. var_dump() 调试 MySQL 独立适配器的 getConnection() getter 返回的值,它基本上是垃圾收集器已经收集的资源的句柄。

这里的垃圾回收发生在var_dump()之前。

总而言之,您提供的第一个测试强制垃圾收集器在代码的第 2 行和第 3 行之间跳转。另一方面,第二个测试在最后强制进行垃圾收集。

结果是你的资源句柄指向已经被 GC 清理的内存。

【讨论】:

  • 非常好的答案!欢迎来到 SO。现在出去教其他新用户变得更像你! ^^ +1
【解决方案2】:

根据提供的代码...

PHP __destruct()

__destruct() 方法,根据文档:

一旦没有对特定对象的其他引用,或在关闭序列期间以任何顺序调用,将立即调用析构函数。

您会遇到不同结果的原因:

$sadb1 = manager::getInstance()->getStandaloneAdapter()->getConnection();
var_dump($sadb1);

给你不正确的结果(你所期望的)是在你的代码中没有更多对 mysql_adapter 实例的引用,因此调用了 __destruct() 方法,这样做会关闭对 resource 的引用持有mysql 链接 - 因为 PHP5 很聪明,并且通过引用自动神奇地传递了大多数东西(好东西 - 大多数时候 ^^),所以当你 var_dump($sadb); 时,__destruct 方法在前面调用行,所以var_dump 给你一个参考,但现在什么都没有。

此代码提供您所期望的原因:

$sadb1 = manager::getInstance()->getStandaloneAdapter();
var_dump($sadb1->getConnection());

是你转储资源,然后然后调用__destruct方法,在转储之后给你debug_trace

我希望这有助于您了解析构函数。

出于好奇,为什么b '悲伤'? ($sadb) ^^

【讨论】:

  • 我的开发团队的不良编码习惯:我们曾经将当前适配器称为 $db(数据库),所以有时我一直使用数据库字而不是适配器,所以:sadb = StandAloneDataBase
  • 啊。谢谢你回复我的笑话! =] 但这很好地解释了为什么b 会伤心,仅基于您回复的前三个词。 ;)
  • 很抱歉我不能接受两个答案,我已经接受了一个对未来 GC 问题更准确的答案:) 但你的答案是正确的,也对我有帮助(以及你的评论:完全!:p)
  • 完全不用担心。此外,他是新来的 SO 并提供了一个该死的好答案,我希望更多新用户做出这样的答案。我很高兴你接受了他的。 =]
猜你喜欢
  • 2021-03-04
  • 1970-01-01
  • 1970-01-01
  • 2010-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-29
  • 1970-01-01
相关资源
最近更新 更多