PHP сборщик мусора при использовании статического метода для создания экземпляра

После долгих поисков я наконец понял, что происходит в моем коде, поэтому этот вопрос не «как мне это исправить«, скорее «почему это происходит?».

Рассмотрим следующий код:

class Foo {
private $id;
public $handle;

public function __construct($id) {
$this->id = $id;
$this->handle = fopen('php://memory', 'r+');

echo $this->id . ' - construct' . PHP_EOL;
}

public function __destruct() {
echo $this->id . ' - destruct' . PHP_EOL;

fclose($this->handle);
}

public function bar() {
echo $this->id . ' - bar - ' . get_resource_type($this->handle) . PHP_EOL;

return $this;
}

public static function create($id) {
return new Foo($id);
}
}

Кажется достаточно простым — при создании он откроет поток памяти и установит свойство $handle а также $id, При разрушении будет использовать fclose закрыть этот поток.

использование:

$foo = Foo::create(1); // works

var_dump( $foo->bar()->handle ); // works

var_dump( Foo::create(2)->bar()->handle ); // doesn't work

Проблема здесь в том, что я ожидаю, что оба вызова будут возвращаться точно так же, но по какой-то причине Foo::create(2) позвони, где я не сохранить экземпляр в переменной вызывает сборщик мусора где-то между return $this часть bar() метод и я на самом деле, используя свойство $handle,

Если вам интересно, этот это выход:

1 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
1 - bar - stream              // echo $this->id . ' - bar - ' ...
resource(5) of type (stream)  // var_dump
2 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
2 - bar - stream              // echo $this->id . ' - bar - ' ...
2 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;
resource(6) of type (Unknown) // var_dump
1 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;

Из того, что я вижу, это то, что происходит:

var_dump( Foo::create(2)->bar()->handle );
// run GC before continuing..  ^^ .. but I'm not done with it :(

Но Зачем? Почему PHP думает, что я закончил с экземпляром переменной / класса и, следовательно, чувствует необходимость его уничтожить?

демос:

eval.in demo
3v4l демо (Только HHVM может понять это — все другие версии PHP не могут)

4

Решение

Это все сводится к refcounts и как относится к PHP ресурсы по-разному.

Когда экземпляр класса уничтожается, все ресурсы, не связанные с базой данных, закрываются (см. Ссылку на ресурсы выше). Все не-ресурсы, на которые есть ссылки в другом месте, будут действительны.

В вашем первом примере вы назначаете $temp = Foo::create(1) что увеличивает пересчет на экземпляр Foo, предотвращая его уничтожение, что делает ресурс открытым.

Во втором примере var_dump( Foo::create(2)->bar()->handle );вот как все складывается:

  1. Foo::create(2) называется, создавая экземпляр Foo,
  2. Вы вызываете метод bar() на новом экземпляре, возвращая $this что увеличивает счет на один.
  3. Ты уходишь bar()Область действия и следующее действие не являются вызовом метода или назначением, refcount уменьшается на единицу.
  4. Подсчет экземпляра равен нулю, поэтому он уничтожен. Все ресурсы, не связанные с базой данных, закрыты.
  5. Вы пытаетесь получить доступ к закрытому ресурсу, возвращая Unknown,

Как дополнительное доказательство, это прекрасно работает:

$temp = Foo::create(3)->bar();
// $temp keep's Foo::create(3)'s refcount above zero
var_dump( $temp->handle );

Как это сделать:

$temp = Foo::create(4)->bar()->bar()->bar();
// Same as previous example
var_dump( $temp->handle );

И это:

// Assuming you made "id" public.
// Foo is destroyed, but "id" isn't a resource.  It will be garbage collected later.
var_dump( Foo::create(5)->id );

это не Работа:

$temp = Foo::create(6)->handle;
// Nothing has a reference to Foo, it gets destroyed, all resources closed.
var_dump($temp);

Ни один не делает это:

$temp = Foo::create(7);
$handle = $temp->handle;
unset($temp);
// $handle is now a reference to a closed resource because Foo was destroyed
var_dump($handle);

когда Foo уничтожен, все открытые ресурсы (кроме ссылок на базы данных) закрыты. Ссылки на другие свойства от Foo все еще действительны.

Демонстрации:
https://eval.in/271514

8

Другие решения

Кажется, все дело в переменной области видимости.

Короче если назначить Foo::create() к глобальной переменной вы можете
получить доступ к handle в глобальном масштабе и деструктор не будет
вызывается до конца сценария.

Принимая во внимание, что если вы на самом деле не назначаете его глобальной переменной, то последняя
вызов метода в локальной области вызовет деструктор; рукоять
закрыт в Foo::create(1)->bar() так ->method сейчас закрыт, когда
вы пытаетесь получить к нему доступ.

Дальнейшее расследование показывает, что предпосылка ошибочна — здесь определенно что-то странное происходит! Это только кажется, влияет на ресурсы.


Случай 1

$foo = Foo::create(1);
var_dump( $foo->bar()->handle );

Результаты в:

resource(3) of type (stream)

В этом случае мы присвоили глобальную переменную $foo быть новым экземпляром Foo создан с Foo::create(1), Теперь мы обращаемся к этой глобальной переменной с bar() вернуть себя, а затем общественности handle,


случай 2

$bar = Foo::create(2)->bar();
var_dump( $bar->handle );

Результаты в:

resource(4) of type (stream)

Опять все нормально, потому что Foo::create(2) создал новый экземпляр Foo а также bar() просто вернул его (он все еще имел доступ к нему в локальной области видимости). Это было присвоено глобальной переменной $bar и это от того, что handle восстанавливается.


случай 3

var_dump( Foo::create(3)->bar()->handle );

Результаты в:

resource(5) of type (Unknown)

Это потому что когда Foo::create() возвращает новый экземпляр Fooиспользуется bar()… однако когда bar() закрывается там больше нет местный Использование этого экземпляра и тому __destruct() вызывается метод, который закрывает дескриптор. Это тот же результат, который вы получили бы, если бы просто написали:

$h = fopen('php://memory', 'r+');
fclose($h);
var_dump($h);

Вы получите точно такой же результат, если попытаетесь:

var_dump( Foo::create(3)->handle );

Foo::create(3) вызовет деструктор, потому что больше нет локальных вызовов к этому экземпляру.


РЕДАКТИРОВАТЬ

Дальнейшее возрождение еще больше замутило воды …

Я добавил этот метод:

public function handle() {
return $this->handle;
}

Теперь, если моя предпосылка была правильной, делаю:

var_dump( Foo::create(3)->handle() );

должен привели к:

resource(3) of type (stream)

… но это не так, снова вы получаете тип ресурса неизвестный — кажется деструктор вызывается в return $this до общедоступный член класса доступен! Тем не менее, совершенно нормально вызывать метод:

public function handle() {
return $this->bar();
}

Это с радостью вернет вам ваш объект:

object(Foo)#1 (2) {
["id":"Foo":private]=>
int(3)
["handle"]=>
resource(3) of type (stream)
}

Кажется, нет способа получить доступ к членам класса ресурсов таким образом, прежде чем вызывается деструктор ?!


Как Алекс Хованский указывает, что со скалярами все в порядке:

public function __destruct() {
$this->id = 2000;
fclose($this->handle);
}

public function handle() {
return $this->id;
}

Сейчас:

var_dump( Foo::create(3)->handle() );

Результаты в:

int(3)

… исходный $ id был возвращен до вызова деструктора.

Это определенно пахнет как ошибка для меня.

2

По вопросам рекламы [email protected]