2

我发现关于 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

哇?!

4

2 回答 2

3

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

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

脚步:

  1. 数据库管理器被分配给变量 $db,
  2. 独立适配器(在这种情况下是 MySQL 适配器,由于您为调试而引入的工厂中的 hack)被分配给 $sadb,
  3. var_dump()getConnection()调试$sadb 独立适配器的方法的返回值,这是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. getConnection()MySQL 独立适配器对象的 getter 的返回值分配给 $sadb,
  3. 因为 MySQL 独立适配器本身没有分配给任何变量,PHP 垃圾收集器决定不再使用它,因此它清理对象并运行其析构函数(析构函数调试首先在您的输出中可见),
  4. var_dump()调试 MySQL 独立适配器的 getter 返回的值getConnection(),它基本上是一个已被垃圾收集器收集的资源的句柄。

这里垃圾收集发生在之前var_dump()

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

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

于 2013-04-23T08:40:45.463 回答
1

根据提供的代码...

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在转储之后为您提供 , 。

我希望这有助于为您解决有关析构函数的问题。

Out of curiosity, why is b 'sad'? ($sadb) ^^

于 2013-04-23T08:45:58.363 回答