Компьютерная помощь
Сайт комнаты "Компьютерная помощь"

Алгоритм наследования классов на PHP

Программирование | 31 июля 2016 г.

Содержание

Статья написана по мотивам одного вопроса на сервисе Ответы@mail.ru. Вопрос и дискуссия расположены по адресу https://otvet.mail.ru/question/192364563. Автор вопроса Сергей Яшановский. В ходе дискуссии появилось желание подробно изучить данный вопрос и написать статью. При подготовке статьи использованы следующие материалы:

  1. PHP Internals Book
  2. Работа с памятью (и всё же она есть)
  3. Подробно об объектах и классах в PHP
  4. Котеров Д.В., Костарев А.Ф. PHP-5. СПб.: “БХВ-Петербург”, 2005
  5. Внутреннее представление значений в PHP7 (часть 1)

Хранение переменных в PHP

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

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

zvalue_value хранит само значение переменной и представляет структуру объединение (union):

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

В зависимости от типа значения, возможен доступ только к одному полю. Тип переменной хранится в zval.type и может иметь следующие значения:

Поле type используемое поле для хранения данных
IS_NULL нет
IS_BOOL long lval
IS_LONG long lval
IS_DOUBLE double dval
IS_STRING struct { char *val; int len; } str
IS_ARRAY HashTable *ht
IS_OBJECT zend_object_value obj
IS_RESOURCE long lval

Теперь мы имеем представление об организации хранения переменных в PHP. Более подробно, особенно про использование refcount__gc и is_ref__gc смотрите в источниках [1,2 и 5]. В дальнейшем я буду ссылаться на zval контейнер и вы уже будете знать что это такое и как используется.

Резюме

  1. Каждая переменная, независимо от типа данных, хранится в едином контейнере, называемый zval.
  2. Этот контейнер имеет все необходимые параметры для работы с переменными во всех возможных режимах.

Простой класс в PHP

Для начала рассмотрим, как же PHP хранит классы. Пример:

class A {
    private $count = 1;

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

Кстати, данный простейший класс из нашего примера в памяти занимает 1160 Байт.

PHP согласно [3] считается транслирующим интерпретатором. Наш скрипт при загрузке в память транслируется в байт-коды и затем полученные байт-коды интерпретируются. Но такие крупные объекты, как классы, в ходе трансляции формируются в определенную структуру zend_class_entry, который хорошо описан в [1] и [3]. А коды методов транслируются в байт коды. Схематично данную структуру можно представить следующим образом:
Хранение класса PHP в памяти
Если внимательно посмотрите на источники, указанные выше, структура zend_class_entry представляет собой солидный объект, где учтены всевозможные варианты использования классов. Я на рисунке указал только те параметры класса, которые могут быть интересны в рамках настоящей статьи.
Разберем структуру.
name — название класса.
parent — ссылка на класс родителя. В нашем случае NULL, поскольку наш класс ни от кого не наследуется.
function_table — массив методов класса. Каждая ячейка этого массива ссылается на объект метода, который описывается структурой zend_function. Эта структура (в упрощенном виде) имеет поля function_name — имя функции, class — ссылка на объект класса, op_array — массив байт-кодов функции и fn_flags, который имеет биты, определяющие область видимости функции (public, protected или private). Также структура function_table содержит поля, описывающие аргументов, их количество, количество обязательных аргументов, список аргументов, в котором каждый аргумент описывается отдельно и другие параметры, но они в этой статье не указаны, поскольку порядок передачи и использования аргументов в методах здесь не рассматриваются.
properties_info — массив описаний свойств класса. Каждая ячейка этого массива ссылается на описание свойства класса. Описание свойства класса представлено структурой zend_property_info. Эта структура содержит поля name — имя свойства, class — ссылка на объект класса, offset — индекс в массиве значений по умолчанию и flags — область видимости свойства (public, protected или private).
default_properties_table — массив значений по умолчанию. Каждая ячейка массива ссылается на контейнер zval, который содержит заданное в коде класса значение. В нашем примере это код

private $count = 1;

Параметр offset в структуре properties_info как раз содержит индекс этого массива. Почему так сделано, будет понятно далее, когда мы перейдем на рассмотрение объектов PHP.
Хоть в статье и не используются, но я показал в структуре класса два параметра, это static_members_table — таблица статических свойств и constants_table — таблица констант класса. В нашем примере статические свойства и константы не используются и поэтому их значение NULL.
Примечание. Поля fn_flags и flags кроме модификатора доступа хранят информацию об статических (static) методах или свойствах.

Резюме

  1. Каждый класс, каждый метод и каждое свойство хранятся в соответствующих структурах и имеют все необходимые параметры для работы с ними.
  2. Каждая структура, описывающая метод или свойство, имеет обратную ссылку на свой класс, тем самым принадлежность метода или свойтсва четко отслеживается.

Наследование классов

Как правило, классы самодостаточные образования и в рамках возложенных задач обеспечивают выполнение всех необходимых операций. Даже если класс объявлен абстрактным, значит в нем подготовлена основа для своих потомков. И наследуемые классы при этом занимаются только своими задачами, а все общие и сторонние задачи остаются в родительском (абстрактном) классе.
Дополним наш пример:

class A {
    private $count = 1;

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

class B extends A {}

Пока что наш класс пустой и все поля, ссылающиеся на массив методов и свойств, имеют значение NULL. В памяти теперь содержатся 2 структуры класса и поле parent класса В теперь ссылается на структуру класса А. Добавим в наш класс В свойство и новый метод.

class A {
    private $count = 1;

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

class B extends A {
    public $count=5;

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

Неожиданно, правда? Такой контрукции на практике, как правило, никто не создает, ибо она не имеет смысла. Но, заданный вопрос по ссылке в начале статьи, в том числе касалось обработки PHP такой конструкции. Ну чтож, попробуем разобраться. Что интересно, PHP ни в ходе трансляции кода, ни в ходе интерпретации при выполнении, никаких ошибок не выдал и это правильно. Класс В абсолютно ничего не знает о свойстве count класса А, поскольку он приватный, то есть доступен только методам своего класса. Для интереса попробуйте поменять область видимости свойств в классах, вы тут же получите ошибку трансляции, ибо вы в наследуемом классе не можете публичное свойство родителя сделать приватным.
В памяти в это время структура класса В стал похож на структуру класса А из рисунка выше и соответственно поле parent ссылается на класс А.

Резюме

  1. Каждый класс хранится в структуре zend_class_entry и его свойства и методы в соответствующих структурах.
  2. При наследовании классов, поле parent наследуемого класса ссылается на структуру класса родителя.

Объекты

В наш код вводим переменные, а именно создадим экземпляр объекта класса В. Код:

$obj = new B();
var_dump($obj);

Вывод:

object(B)#1 (2) {
  ["count"]=>
  int(5)
  ["count":"A":private]=>
  int(1)
}

Как видите, в объекте хранятся свойства обоих классов со своими значениями. Но второе свойство имеет указание, что это свойство класса А.
Что при этом произошло в памяти. Посмотрим на рисунок.


Алгоритм наследования на PHP. Объекты
При создании нашего объекта что происходит:

  1. В памяти создается структура объекта zend_object.
  2. В поле ce вносится ссылка на класс В.
  3. Поле properties содержит массив объявленных свойств во всех классах, начиная с родительского. Каждый элемент массива содержит ссылку в объект zend_property_info классов.
  4. А поле properties_table является символьной таблицей объекта и содержит ссылки на zval контейнеры. При этом ключами элементов массива являются названия свойств. Обратите внимание, первый элемент обозначен как \0A\0сount (здесь \0 обозначает символ #00, то есть нулевой байт). В этой записи А означает название класса А, а сount соответственно название свойства. Такая запись означает, что count является приватным свойством класса А и доступен только методам данного класса. Второй элемент просто обозначен своим названием, поскольку он публичное свойство и второго такого во всей цепочке не может быть. Что интересно, если свойство count класса В обозначить как protected, то запись в properties_table была бы такой \0 * \0сount, вместо названия класса знак *. Это значит, что свойство доступно всем классам цепочки, но не извне. Такое свойство извне не видно. Кстати, эти же записи показаны в выводе функции var_dump($obj);, только в более читабельном виде.
  5. Поле guards используется для защиты от циклических вызовов в методах объекта.
  6. Созданный объект размещается в хранилище объектов zend_object_store, который обеспечивает хранение объекта в единственном экземпляре. Здесь эту структуру рассматривать не будем, за подробностями обратитесь к источникам.
  7. Объект привязывается к имени переменной.

Дополним наш код:

echo $obj->getCount(); // выводит 1
echo $obj->getCountB(); // выводит 5

Почему так происходит. При обращении к переменной в методе класса, PHP начинает просмотр массива поля properties_table объекта. Расшифровывает запись \0A\0сount, получает имя класса А и имя переменной count. При случае, если это метод getCount(), то все условия выполняются и значение переменной по ссылке проставляется в выражение. Во второй строке кода запись \0A\0сount не удовлетворяет условиям (метод getCountB() относится к классу В) и PHP в выражении использует значение публичного свойства count, который удовлетворяет всем условиям.
Обратите внимание, на рисунке элементы массива properties_table ссылаются на zval контейнеры значений по умочанию соответствующих классов. Вспомните, раздел настоящей статьи Простой класс в PHP. Для того, чтобы не расходовать лишнюю память, первоначально объект ссылается на значения по умолчанию класса. Но стоит нам изменить какое-либо свойство, в памяти создается новый zval контейнер с новым значением.
А теперь попробуем нашему объекту назначить динамическое свойтво. Код:

$obj->newVar = "Это свойство не определено в классах А и В.";
var_dump($obj);

Вывод:

object(B)#1 (3) {
  ["count":protected]=>
  int(5)
  ["count":"A":private]=>
  int(1)
  ["newVar"]=>
  string(77) "Это свойство не определено в классах А и В."
}

Как видите, наше свойство newVar успешно добавлено в объект и оно определено как публичное свойство (public). Новое свойство добавилось в таблицу свойств properties_table как новый элемент массива со своим контейнером zval.


Добавление нового динамического свойства в объект
А как объекты видят статические свойства? Изменим код нашего класса В:

class B extends A {
    public $count=5;
    static $staticVar = 7;

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

$obj = new B();
var_dump($obj);

Вывод:

object(B)#1 (2) {
  ["count"]=>
  int(5)
  ["count":"A":private]=>
  int(1)
}

Как видите, в таблице свойств объекта статического свойства нет. И, соответственно, обращение к этому свойству от объекта приведет к ошибке:

echo $obj->staticVar;

Вывод:

PHP Notice:  Undefined property: B::$staticVar in /home/shah/projects/objects/test1.php on line 21

Резюме

  1. Объекты, в отличие от классов, занимают очень мало места в памяти. Правда, по мере присваивания новых значений свойствам объекта, под них выделяются отдельная память и размер объекта увеличивается.
  2. Объект хранит ссылки на структуру описание свойства классов, и отдельно, таблицу переменных объекта.
  3. При обращении к свойствам в методах объекта, элементы массива перебираются последовательно и первое свойство, отвечающее всем условиям (имя переменной, видимость для метода текущего класса) используется в выражении.
  4. Статические свойства доступны только при обращении как к свойству класса $A::staticVar.
twitter.com facebook.com vkontakte.ru odnoklassniki.ru mail.ru yandex.ru

При цитировании, копировании, клонировании материалов с сайта целиком или частично, ссылка на страницу, откуда был скопирован материал, обязательна! При нарушении данных условий прошу незамедлительно удалить со своих ресурсов скопированный материал. Администрация сайта.

Комментариев: 5
    

    Комментариев: 5

    •  Яшановский Сергей | 2 августа 2016 в 15:03:08 (ссылка)

      Спасибо огромнейшее за статью. Теперь мне почти понятен принцип наследования. Но осталось пару нюансов.

      Рассмотрим код (его мы уже с вами рассматривали ранее в Ответах mail):

      class A {
       public $count = 2;
      }
      class B extends A {
       private $count = 3;
      }

      В таком случае в properties_table объекта будет 2 значения: count и \0B\0 count? Но в примере выше будет ошибка - по какой тогда причине? Ведь, например, при вызове count элементы массива перебираются последовательно. Если, например, вызвать метод, который будет возвращать count (его в коде нет, но представим, что он есть) с родителя - интерпретатор должен выдать 2, а если с наследника - 3. Это по логике статьи. Но такого не будет - интерпретатор даже на этапе построения кода выдаст ошибку. Вообще, я не допонял это из-за того, что я не понимаю до конца смысл переопределения. А в статье про него не написано вроде.

      Вот если в примере выше заменить private на public, то что будет происходить со структурой, что Вы описывали в статье? Тогда сначала в массив properties_table объекта запишется public свойство count, а потом ОНО же переопределится? (т.е. изменится значение, а не добавится аналогичный новый ключ с таким же именем). Если так, то что будет происходить при переопределении protected свойства на public? Оно тоже будет считаться как одно, и переопределится, грубо говоря, область видимости у свойства объекта? Допустим. Но почему тогда нельзя переопределить свойство с public на protected? И вообще, почему нельзя сузить область видимости, а расширить можно? Да как блин происходит это переопределение?)

      •  Яшановский Сергей | 2 августа 2016 в 16:20:40 (ссылка)

        "интерпретатор должен выдать 2, а если с наследника - 3" - точнее, наоборот, ведь сначала в массив записываются свойства прототипа, а потом уже наследника. Это выглядит глупо, но работать должно, однако не работает. И да, каким образом переопределяются методы? В них типо ссылка на вызов переопределяется?

        •  Яшановский Сергей | 2 августа 2016 в 16:23:39 (ссылка)

          "интерпретатор должен выдать 2, а если с наследника - 3" - стоп. Тогда интерпретатор должен выдать 2 раза двойку, так как public count будет 2 раза подходить по условию (а до private свойства алгоритм и не дойдет). В этом наверно конфликт? Конфликт логики? Но вопрос про переопределение с public на protected остается открытым...Как и те, в принципе, потому что я думаю, что понимаю это неправильно.

      •  shah | 2 августа 2016 в 23:09:22 (ссылка)

        Привет)

        В приведенном коде возникает ошибка не выполнения, а трансляции. PHP при трансляции кода во внутреннее представление, уже анализирует код и одновременно создает объекты (структуры) классов.

        Сначала создает структуру класса родителя (поскольку он идет первым в потоке кода). И все созданное родителя принимает за веру (сравнивать то по ни с чем). Следующим делом переходит за создание класса потомка. О том, что текущий (второй) класс потомок он (php) узнает по ключевому слову extends и соответственно, получает ссылку на объект (структуру) родителя.

        Вот теперь, PHP уже анализирует каждое заявленное свойство и метод. Статические свойства или методы при любых нелепых описаниях и вариациях, php за ошибку не засчитает, ибо они доступны только через имя класса: A::static.

        Это значит, что php, встретив в потомке свойство public $count, начнет искать такое же свойство у родителя. Причем строго по имени, а не зашифрованной записью типа \0B\0count. Почему? Потому что такая запись используется только в объекте. Если такого свойства у родителя нет (php ищет в соответствии с модификаторами доступа текущего класса), то создание свойства разрешается. А если у родителя такое же, но публичное (или protected) свойство уже доступно, то создание любого свойства, с таким же доступом или сужающего доступ, чем есть у родительского класса, запрещается.

        Почему? В данном случае, у нас появляется конфликтная ситуация. При обращении методов класса к этому свойству, метод затруднится, к какому свойству обращаться, поскольку оба свойства доступны для чтения и записи. Так какое свойство выбрать? Я затрудняюсь решить, ибо такого механизма, как для методов в php нет. Это в методах потомка к такому же методу родителя можно обратиться конструкцией parent::someMethod. Для свойств такого нет.

        В случае, когда у родителя приватное свойство,а у потомка публичное, вполне нормально. PHP считает, что приватное свойство создается только для собственных целей, то есть, для целей текущего класса. А публичные свойства для всеобщего пользования. protected свойства для целей всей цепочки. И поэтому когда у родительского класса приватное свойство, то его метод приоритетно обращается в собственному свойству.

        •  Яшановский Сергей | 3 августа 2016 в 12:25:22 (ссылка)

          Спасибо, Вы меня выручаете очень! Редко увидишь сейчас людей, которые бесплатно могут помочь человеку. И у меня, надеюсь, последний вопрос (или утверждение) остался...

          А если у родителя такое же, но публичное (или protected) свойство уже доступно, то создание любого свойства, с таким же доступом или сужающего доступ, чем есть у родительского класса, запрещается.

          А как же переопределение? Если в родителе есть public свойство, а в наследнике тоже такое же public, то свойство переопределится. Правильно ли я понимаю, что при переопределении в object скопируются все свойства с родителя (в соответствии с модификаторами доступа), а затем в объект "наложатся" свойства с дочернего класса. А переопределятся они, если свойства имеют одинаковое имя (естественно) + модификаторы либо такие же (кроме private), либо расширяющие область видимости. А вот с переопределением методов, я так понимаю, по-другому. Тут переопределяются не сами методы, а ссылка на вызов, поскольку методы находятся в коде класса. Я примерно правильно понял ведь? :)

    Оставьте комментарий!

    Используйте нормальные имена

    Вы можете войти под своим логином или зарегистрироваться на сайте.

    (обязательно)