Сложное поведение с PHP (5.3), статическое наследование и ссылки

Я пишу библиотеку в PHP 5.3, основная часть которой представляет собой класс с несколькими статическими свойствами, который расширяется из подклассов, чтобы позволить zero-conf для дочерних классов.

В любом случае, здесь образец, чтобы проиллюстрировать обнаруженную мной особенность:

<?php

class A {
    protected static $a;
    public static function out() { var_dump(static::$a); }
    public static function setup($v) { static::$a =& $v; }
}
class B extends A {}
class C extends A {}

A::setup('A');
A::out(); // 'A'
B::out(); // null
C::out(); // null

B::setup('B');
A::out(); // 'A'
B::out(); // 'B'
C::out(); // null

C::setup('C');
A::out(); // 'A'
B::out(); // 'B'
C::out(); // 'C'

?>

Теперь это довольно желаемое поведение для статического наследования, насколько мне известно, однако, меняя static::$a =& $v; на static::$a = $v; (без ссылки), вы получаете ожидаемое поведение, то есть:

'A'
'A'
'A'

'B'
'B'
'B'

'C'
'C'
'C'

Может кто-нибудь объяснить, почему это? Я не могу понять, как ссылки влияют на статическое наследование любым способом:/

Update:

Основанный на ответе Artefacto, имеющий следующий метод в базовом классе (в данном случае, A) и вызывающий его после объявления класса, создает поведение, обозначенное как ' желательно "выше" без необходимости назначать по ссылке в сеттерах, оставляя результаты при использовании self:: как "ожидаемое" поведение выше.

/*...*/
public static function break_static_references() {
    $self = new ReflectionClass(get_called_class());
    foreach($self->getStaticProperties() as $var => $val)
        static::$$var =& $val;
}
/*...*/
A::break_static_references();
B::break_static_references();
C::break_static_references();
/*...*/

Ответ 1

TL; версия DR

Статическое свойство $a является другим символом в каждом из классов, но на самом деле это одна и та же переменная в том смысле, что в $a = 1; $b = &$a;, $a и $b есть одна и та же переменная (т.е. они 're в том же наборе ссылок). При выполнении простого назначения ($b = $v;) значение обоих символов будет изменено; при выполнении задания по ссылке ($b = &$v;) будет затронуто только $b.

Оригинальная версия

Во-первых, давайте понять, как статические свойства "унаследованы". zend_do_inheritance выполняет итерацию статических свойств суперкласса, вызывающих inherit_static_prop:

zend_hash_apply_with_arguments(&parent_ce->default_static_members TSRMLS_CC,
    (apply_func_args_t)inherit_static_prop, 1, &ce->default_static_members);

Определение которого равно:

static int inherit_static_prop(zval **p TSRMLS_DC, int num_args,
    va_list args, const zend_hash_key *key)
{
    HashTable *target = va_arg(args, HashTable*);

    if (!zend_hash_quick_exists(target, key->arKey, key->nKeyLength, key->h)) {
        SEPARATE_ZVAL_TO_MAKE_IS_REF(p);
        if (zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p,
                sizeof(zval*), NULL) == SUCCESS) {
            Z_ADDREF_PP(p);
        }
    }
    return ZEND_HASH_APPLY_KEEP;
}

Переведите это. PHP использует копию при записи, что означает, что он попытается использовать одно и то же представление реальной памяти (zval) значений, если они имеют одинаковый контент. inherit_static_prop вызывается для каждого из статических свойств суперкласса, поэтому его можно скопировать в подкласс. Реализация inherit_static_prop гарантирует, что статические свойства подкласса будут ссылками PHP, независимо от того, является ли разделение zval родителя (в частности, если суперкласс имеет ссылку, ребенок будет делиться zval, если он не работает 't, zval будет скопирован, а новый zval станет ссылкой, второй случай нас действительно не интересует).

Таким образом, в основном, когда формируются A, B и C, $a будет другим символом для каждого из этих классов (т.е. каждый класс имеет свою хэш-таблицу свойств, и каждая хэш-таблица имеет свою собственную запись для $a), НО базовый zval будет тем же И это будет ссылка.

У вас есть что-то вроде:

A::$a -> zval_1 (ref, reference count 3);
B::$a -> zval_1 (ref, reference count 3);
C::$a -> zval_1 (ref, reference count 3);

Поэтому, когда вы выполняете обычное назначение

static::$a = $v;

поскольку все три переменных имеют один и тот же zval и его ссылку, все три переменные будут принимать значение $v. Было бы так же, если бы вы сделали:

$a = 1;
$b = &$a;
$a = 2; //both $a and $b are now 1

С другой стороны, когда вы делаете

static::$a =& $v;

вы будете нарушать набор ссылок. Скажем, вы делаете это в классе А. У вас теперь есть:

//reference count is 2 and ref flag is set, but as soon as
//$v goes out of scope, reference count will be 1 and
//the reference flag will be cleared
A::$a -> zval_2 (ref, reference count 2);

B::$a -> zval_1 (ref, reference count 2);
C::$a -> zval_1 (ref, reference count 2);

Аналогичным было бы

$a = 1;
$b = &$a;
$v = 3;
$b = &$v; //$a is 1, $b is 3

Работа вокруг

Как показано в Gordon, теперь удаленный ответ, набор ссылок между свойствами трех классов также может быть разбит путем повторного использования свойства в каждом из классов:

class B extends A { protected static $a; }
class C extends A { protected static $a; }

Это связано с тем, что свойство не будет скопировано в подкласс из суперкласса, если оно будет обновлено (см. условие if (!zend_hash_quick_exists(target, key->arKey, key->nKeyLength, key->h)) в inherit_static_prop).