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

Алгоритм наследования классов на 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), либо расширяющие область видимости. А вот с переопределением методов, я так понимаю, по-другому. Тут переопределяются не сами методы, а ссылка на вызов, поскольку методы находятся в коде класса. Я примерно правильно понял ведь? :)

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

    grin LOL cheese smile wink smirk rolleyes confused surprised big surprise tongue laugh tongue rolleye tongue wink raspberry blank stare long face ohh grrr gulp oh oh downer red face sick shut eye hmmm mad angry zipper kiss shock cool smile cool smirk cool grin cool hmm cool mad cool cheese vampire snake excaim question

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

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

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