Лабораторная работа №5. Объекты и классы в PHP
Лабораторная работа №5. Объекты и классы в PHP
Класс как тип данных
До сих пор в программах мы оперировали переменными, хранящими значения определенного типа. В основном использовались типы string (строка) и douЫe (вещественное число), реже- array (ассоциативный массив). Для работы с такими переменными существует целый ряд операций: арифметические - для чисел; strlen (), substr () и т. д. - для строк; count (), array _merge () и др. - для массивов. ООП позволяет нам вводить новые типы данных в дополнение к уже существующим.
Мы видим, что с каждой переменной (а точнее, с каждым типом данных) логически связаны данные двух видов: во-первых, это некоторый набор битов, представляющий само значение переменных, а во-вторых, набор функций и операторов, предназначенных для обработки этих битов. Легко видеть, что любой тип всегда может быть полностью описан в терминах данных и операций над ними.
Ключевым понятием ООП является класс. Класс можно рассматривать как тип некоторой переменной в том понимании, которое было описано в предыдущем абзаце.
Переменная класса (далее будем ее называть объектом класса) обычно имеет набор свойств (значений различных типов) и операций (или методов, функций), которые могут быть с ним проведены. Свойства и методы класса часто называют его членами.
Рис. 1. Переменные объявляются при помощи типа, объекты - при помощи класса
Так же как может существовать много переменных одного и того же типа (например, строкового), не связанных между собой, возможно и наличие в программе множества объектов одного и того же класса, различающихся своими свойствами.
Например, мы можем рассматривать тип int как класс. Тогда переменная этого "класса" будет обладать одним-единственным свойством (ее целым значением), а также набором методов (сложение, вычитание, инкремент и т. д.). При этом методы выглядят как арифметические операторы+,-,++ и т. д.
В языке С++ мы могли бы, действительно, объявить новый тип int именно таким образом. Однако в РНР дело обстоит немного хуже: мы не имеем права переопределять стандартные операции (сложение, вычитание и т. д.) для объектов. Например, если бы мы захотели добавить в язык комплексные числа, в С++ это можно было сделать без особых затруднений (и класс комплексных чисел по использованию практически не отличался бы от встроенного типа int), однако в РНР нам такое добавление не удастся.
Альтернативное решение состоит в том, чтобы везде вместо + и других операций использовать вызовы соответствующих функций, например, add ( ) , которые бы являлись методами класса. Собственно, только такой способ организации методов и поддерживается в РНР (а также в Java).
Подход к созданию классов, применяемый в объектно-ориентированных языках, называют инкапсуляцией. Данные, принадлежащие классу, сохраняются в его свойствах, доступ к которым тщательно ограничивается и предоставляется в основном при помощи специальных методов.
Создание нового класса
Новый класс (тип данных) в программе описывается при помощи ключевого слова class. Внутри класса могут располагаться его свойства (переменные класса) и методы (функции-члены класса).
Давайте для тренировки опишем класс с именем мathComplex, объекты которого будут хранить комплексные числа (листинг 1). Этот класс пока поддерживает только сложение и вычитание чисел.
ПРИМЕЧАНИЕ
В математике комплексным числом называют пару двух вещественных чисел, первое из которых условно называют "действительной частью", а второе - "мнимой частью" комплексного числа. Все действительные числа соответствуют комплексным величинам с мнимой частью, равной нулю. Квадратный корень из -1, не существующий в виде действительного числа, имеет комплексное значение (0, 1), которое еще иногда обозначают знаком i. С комплексными числами можно выполнять все те же операции, что и с действительными - складывать, умножать, делить и т. д.
Листинг 1. Пример класс. Файл Math/Complex.php
Как видно из листинга 1, для объявления членов класса $re и $im мы воспользовались модификатором public, который более подробно будет освещен в следующих темах. "Добраться" до членов класса можно при помощи специальной переменной $this, которая всегда существует внутри методов (функций-членов) класса.
Файл, приведенный в листинге 1, при своем включении не выполняет никаких действий. Его задача - добавить в программу новый класс с именем мathComplex. В один файл можно добавлять множество классов, однако для облегчения поиска классов принято придерживаться рекомендации: один файл - один класс.
Работа с классами
Предположим, что в программе каким-то образом уже описан некоторый класс. Так как класс - это, по сути, тип данных, мы должны иметь некоторый механизм для создания переменных, хранящих значение этого типа.
Доступ к свойствам объекта
Как ранее говорилось, каждый объект имеет свой собственный набор ассоциированных с ним свойств (значений, или переменных) и множество методов (функций-членов).
Каждое свойство объекта доступно в программе по его имени. Можно присваивать значение свойству или получать его величину:
Как видите, доступ к свойству осуществляется при помощи оператора -> (стрелка, символ -, за которым идет > ).
Обратите внимание, что объект очень похож на ассоциативный массив. В самом деле, в массиве ведь тоже может храниться несколько значений, доступ к каждому из которых осуществляется по имени его ключа. У объектов вместо ключей - имена свойств, а для доступа к значениям используется оператор ->, а не квадратные скобки.
Доступ к методам
Вспомним, как мы вызывали "методы" встроенных типов данных:
Как видите, для встроенных типов используется либо операторная запись вызова "метода" (например, сложение), либо же функциональная (как будто вызывается функция). В РНР для вызова метода некоторого объекта используется оператор "стрелка".
Листинг 2 Вызов метода объекта. Файл call.php
Давайте посмотрим, что происходит, когда мы вызываем метод класса. Первым делом создается локальная переменная $this, которой присваивается то же значение, что было у $obj. То есть, в $this теперь хранится ссылка на объект, для которого вызывается метод. Далее РНР смотрит, какому классу принадлежит $obj (в нашем случае это MathCornplex), и находит функцию-член: MathCornplex: : add (). Функция вызывается, при этом $this, напомним, равен $obj. В итоге add() изменяет значения $obj->re и $obj->im (которые для нее выглядят как $this->re и $this->im. Их мы распечатываем следующей строчкой программы, уже после выхода из функции.
Как видите, вызов метода некоторого объекта автоматически предоставляет ему доступ к свойствам этого объекта посредством специальной переменной $this. При этом $this не нужно нигде объявлять явно, она появляется сама собой. Данная техника - ключевая особенность ООП.
ПРИМЕЧАНИЕ
В отличие от таких языков, как С++ и Java, в РНР не поддерживается создание в одном классе нескольких методов с одинаковым именем, которые бы различались только типами и количеством аргументов. Поэтому-то нам и пришлось создавать класс MathComplex1, а не просто добавить новую функцию add () с аргументом типа MathComplex в имеющийся класс.
Перегрузка преобразования в строку
Посмотрите еще раз на листинг 3. Возможно, вы спросите: почему мы назвали функцию _tostring() столь длинным именем? И зачем эти неуклюжие символы подчеркивания?
Оказывается, в РНР существует ряд имен методов, начинающихся с двойных подчерков, которые имеют специальное значение. Мы только что затронули один из них: это функция __toString (). Она вызывается РНР автоматически всякий раз, когда мы затребуем неявное преобразование ссылки на объект в строку.
Листинг 5 Перегрузка интерполяции. Файл tostring.php
Обратите внимание, что мы вставляем объект $а прямо в строку, и в момент интерполяции переменных РНР вызывает метод __tostring (). Результат будет таким:
Значение: (314, 101)
Если бы не метод __toString () (например, при использовании класса MathComplex, который мы написали в самом начале этой главы), вывод был бы другим:
Catchable fatal error: Object of class MathComplex could not bе converted to string
Как видите, РНР генерирует ошибку, в которой сообщает о невозможности преобразования объекта класса МathComplex в строку.
Инициализация и разрушение
Давайте еще раз взглянем на листинги 4 и 5. Как видите, для корректного создания объекта нам недостаточно просто использовать оператор new: потом приходится еще инициализировать свойства объекта ($re и $im). Конечно, это утомительно, и о присваивании легко случайно позабыть, - в результате будет ошибка. В нашем примере инициализация очень проста, однако в реальной ситуации она может быть, наоборот, весьма объемна (например, если класс требует загрузки каких-нибудь файлов или записей из базы данных).
Конструктор
Давайте взглянем на очередную реализацию нашего класса комплексных чисел (листинг 6).
Листинг 6 Пример класса с конструктором. Файл Math/Complex2.php
Обратите внимание на необычное название метода - _ construct () . Это так называемый конструктор класса. Он вызывается всякий раз, когда вы используете оператор new для объекта.
ПРИМЕЧАНИЕ
В отличие от других языков программирования, в РНР у класса может быть только один конструктор.
Как видите, конструктор принимает два параметра: действительную и вещественную части комплексного числа. Листинг 7 иллюстрирует применение данного класса.
Листинг 7 Использование конструктора. Файл construct.php
Насколько легче стало создание новых объектов! Теперь мы уже при всем желании не сможем пропустить их инициализацию - конструктор будет вызван в любом случае.
Параметры по умолчанию
Как и для обычных функций и методов, для конструкторов можно задавать параметры по умолчанию. Например, объявив его следующим образом:
Мы заставим РНР корректно воспринимать следующие четыре команды:
При этом недостающие параметры будут заполнены значениями по умолчанию (в нашем примере это 0).
В примере, который только что был приведен, по умолчанию создается объект класса MathCornplex2 со значением (0, 0). В языках программирования вроде Java и С++ конструктор класса, который допускает создание объектов без указания параметров, называется конструктором по умолчанию.
Деструктор
До сих пор мы только создавали новые величины (строки, массивы, числа) и объекты в программе на РНР, не задумываясь о том, что с ними происходит, когда они нам больше не нужны. В то же время, вопрос разрушения объектов и удаления их из памяти в ООП играет очень важную роль. Рассмотрим его чуть подробнее.
Описание деструктора
По аналогии с конструкторами обычно рассматриваются деструкторы. Деструктор - специальный метод объекта, который вызывается при уничтожении этого объекта (например, после завершения программы). Деструкторы обычно выполняют служебную работу - закрывают файлы, записывают протоколы работы, разрывают соединения, "форматируют жесткий диск" - в общем, освобождают ресурсы.
Деструктор - это специальный метод класса с именем _ destruct () , который будет гарантированно вызван при потере последней ссылки на объект в программе. Так как деструктор запускается самим РНР, он не должен принимать никаких параметров.
В листинге 11 приведен модифицированный класс с именем FileLogger, в котором объявляется деструктор. Теперь нам уже нет необходимости заботиться о "ручном" вызове close () в программе - РНР выполняет "финализирующие" действия самостоятельно.
Листинг 11 Деструктор. Файл File/Logger.php
Листинг 12 Использование класса с деструктором. Файл destr.php
Посмотрите на листинг 12. В нем мы последовательно создаем 10 объектов класса FileLogger, полагаясь на то, что их деструкторы будут вызваны в нужное время. Зададимся важным вопросом: в какой именно момент это произойдет? Интуитивно понятно, что деструктор вызывается в тот момент, когда объект в программе больше не нужен, и память, отведенную под него, можно освободить. Это событие в нашем случае происходит при перезаписи переменной $logger, т. е. когда ей присваивается новое значение (первая команда цикла).
Но задумайтесь, как РНР определяет, когда объект больше не нужен и его можно удалять из памяти? В нашем случае все просто: в единицу времени на объект ссылается лишь одна переменная $logger, но представьте, что бы произошло, если бы это оказалось не так. Например, мы можем накапливать объекты FileLogger в каком-нибудь массиве, и тогда уже на каждом обороте цикла вызова деструктора не произойдет:
Если вы модифицируете программу таким образом, то обнаружите, что она попрежнему работает! Записи в lоg-файл test.log добавляются, причем их очередность остается той же, что была ранее. Но в какой же момент РНР вызывает деструкторы десяти объектов в этом случае?
Алгоритм сбора мусора
Как мы знаем, в РНР существует такое понятие, как ссылка на объект. Ссылочная переменная хранит не сам объект, а лишь его адрес в памяти - таким образом, на один и тот же объект могут ссылаться сразу несколько переменных. Забегая вперед скажем, что объекты, на которые в программе не осталось ссылок, РНР немедленно удаляет из памяти (предварительно вызвав деструкторы). Вся специфика заключена в словах "не осталось ссылок" и "немедленно".
ПРИМЕЧАНИЕ
Представьте, что объект - это пальто, сданное в гардероб. Тогда в качестве ссылки будет выступать номерок на это пальто, выдаваемый гардеробщиком. Этот номерок можно "копировать" - например, отдав в мастерскую (аналог присваивания переменных). При этом пальто остается тем же самым и не изменяется. Что произойдет с пальто, если человек уничтожит свой номерок (обнулит ссылку на объект)? .. Наверное, через некоторое время гардеробщик сообразит, что пальто больше не нужно и лишь занимает вешалку, и отправит его на утилизацию - диспетчер динамической памяти (или, как его еще называют, сборщик мусора) удалит объект-пальто. Однако РНР гораздо "шустрее": если гардеробщику требуется некоторое время на принятие решения, то интерпретатор сразу же обнаруживает объекты, на которые нет ссылок, и удаляет их, не задерживаясь.
Сложности начинаются, когда на некоторый объект имеется более одной ссылки. В этом случае, конечно же, уничтожение нужно провести только при обнулении последней ссылки, но ни в коем случае - промежуточных. Но как же определить, ссылается ли кто-то еще на объект или же нет? ..
В этом и заключена специфика алгоритма со счетчиком ссылок, применяемого в РНР (а также в Perl), одновременно его сила и слабость. Любой объект, который вы создаете, содержит в себе скрытое поле, хранящее так называемый счетчик ссылок. Каждый раз, когда в программе появляется новая ссылка на объект, этот счетчик увеличивается на 1 ( обычно это происходит при выполнении операции присваивания $alias = $source: раньше ссылка хранилась только в $source, а теперь и в $alias, и в $source). Соответственно, при удалении ссылки счетчик уменьшается на 1. Например, операция unset ( $alias), $alias - "что угодно", а также выход локальной переменной функции за область видимости приводит к потере ссылки на объект, которая раньше находилась в $alias. Ясно, что при обнулении счетчика на объект больше никто не ссылается, а потому его можно спокойно удалить из памяти, что РНР и делает. Таким образом, объект удаляется после некоторой операции присваивания, приводящей к потере последней ссылки на него.
Удаление объекта или массива - довольно сложная процедура. Интерпретатору необходимо:
· удалить все ссылки, которые содержит сам этот объект (например, при удалении массива нужно обнулить все элементы, которые в нем содержатся - на случай, если они сами являются объектами). Если в процессе этой операции какой-то другой подчиненный объект теряет последнюю ссылку, то он также будет удален, и т. д. -рекурсивно;
· вызвать деструктор; деструкторы играют весьма важную роль в ООП, так что полная их поддержка в алгоритме со счетчиком ссылок - это сильная сторона метода;
· освободить занимаемую память; эта операция выполняется в самый последний момент и может рассматриваться как низкоуровневая.
Циклические ссылки
Алгоритмы сборки мусора с использованием счетчика ссылок, как правило, имеют один очень существенный недостаток. Речь идет о циклических ссылках. Давайте рассмотрим пример (листинг 13).
Листинг 13 Проблемы алгоритма со счетчиком ссылок. Файл refcount.php
В программе создаются два объекта: $father и $child. При этом объект-отец хранит ссылки на всех своих потомков, а каждый сын - ссылку на отца. Это и называется циклическими ссылками: если идти "вдоль них", мы никогда не остановимся. Циклические ссылки встречаются на практике очень часто, особенно при описании иерархических структур.
Теперь взгляните на предпоследнюю строчку кода. Мы присваиваем ссылочным переменным $father и $child значение NULL, в результате чего счетчик ссылок в соответствующих объектах уменьшается на 1.
А теперь - "сюрприз": несмотря на то, что в программе мы уже никак не сможем "добраться" до данных объектов $father и $child (мы же уничтожили эти ссылки), память для них все же не освобождается, и они остаются "висеть" мертвым грузом, хотя к ним уже и нельзя получить доступ! Убедиться в этом можно, запустив скрипт листинга 13 в браузере:
Пока что все живы ... Убиваем всех.
все умерли, конец программы.
Father умер.
Child умер.
Как видите, сообщение "Все умерли", выводящееся в конце программы, оказывается самым первым, а не последним по списку. Это означает, что деструкторы были вызваны уже после завершения работы скрипта.
Давайте теперь в качестве эксперимента уберем строчку: $father->children[] = $child. Таким образом, теперь в программе уже не будет кольцевых ссылок, и результат ее работы станет выглядеть так:
Пока что все живы ... Убиваем всех.
Child умер.
Father умер.
Все умерли, конец программы.
Как видите, если циклических ссылок в программе нет, объекты уничтожаются в правильном порядке.
Проблема циклических ссылок
Все дело в злополучных счетчиках ссылок. Смотрите: $father ссылается на $child, а $child - на $father. Это значит, что и у того, и у другого счетчик равен 1 (ведь на каждого из них ссылается другой)! Стоит ли удивляться, что сборщик мусора не сработал? .. Ведь в программе не осталось ни одного объекта с нулевым счетчиком ссылок.
ПРИМЕЧАНИЕ
Аналогия с гардеробом: вы сдаете в него свое пальто, а также чужое (которое взяли только что, например, по поддельному номерку). При этом (для конспирации) номерок от своего пальто вы кладете в карман чужого, а от чужого - в карман своего. Проделав данную махинацию, вы обнаружите, что не можете больше получить одежду!
Мы рассмотрели пример циклических ссылок с "длиной цикла", равной двум. Однако, конечно, РНР попадает в безвыходную ситуацию и в случае большей косвенности: А ссылается на В, В ссылается на С, С ссылается на А.
ПРИМЕЧАНИЕ
Еще одна аналогия - известная безвыходная ситуация "ключи от машины в квартире, ключи от квартиры в сейфе, ключи от сейфа - в машине".
Еще более примечателен по своей простоте следующий код:
Чем не замкнутая внутри себя вселенная? .. Мы получили объект, который, несмотря на потерю последней ссылки в программе, все равно продолжает существовать в памяти, занимая место, но не будучи доступным.
Итак, общий вывод: алгоритм сборки мусора и автоматического вызова деструкторов попросту "не срабатывает", когда в программе имеются кольцевые ссылки.
Проблема утечки памяти в результате циклических ссылок была неразрешима до версии РНР 5.3, начиная с которой в сборщик мусора РНР внедрен синхронный механизм сбора циклических ссылок.
Вкратце, все объекты, генерирующие ссылки, помещаются в специальный буфер, который называется корневым. При заполнении буфера (а его размер составляет 10000) стартует процедура сборки мусора, в результате которой происходит обход дерева всех ссылающихся элементов, алгоритм разрешает циклы и корректирует счетчики. Объекты, чьи счетчики стали равны нулю, удаляются. Механизм довольно ресурсоемок и включается только по заполнению буфера. По умолчанию сборщик мусора включен; если ваши скрипты работают короткое время и потребляют мало памяти, можно увеличить производительность за счет отключения сборщика мусора, установив значение директивы\ zend.enable_gc в конфигурационном файле php.ini в off.
Задания для самостоятельного решения
Лабораторная работа №5. Объекты и классы в PHP
Класс как тип данных
До сих пор в программах мы оперировали переменными, хранящими значения определенного типа. В основном использовались типы string (строка) и douЫe (вещественное число), реже- array (ассоциативный массив). Для работы с такими переменными существует целый ряд операций: арифметические - для чисел; strlen (), substr () и т. д. - для строк; count (), array _merge () и др. - для массивов. ООП позволяет нам вводить новые типы данных в дополнение к уже существующим.
Мы видим, что с каждой переменной (а точнее, с каждым типом данных) логически связаны данные двух видов: во-первых, это некоторый набор битов, представляющий само значение переменных, а во-вторых, набор функций и операторов, предназначенных для обработки этих битов. Легко видеть, что любой тип всегда может быть полностью описан в терминах данных и операций над ними.
Ключевым понятием ООП является класс. Класс можно рассматривать как тип некоторой переменной в том понимании, которое было описано в предыдущем абзаце.
Переменная класса (далее будем ее называть объектом класса) обычно имеет набор свойств (значений различных типов) и операций (или методов, функций), которые могут быть с ним проведены. Свойства и методы класса часто называют его членами.
Рис. 1. Переменные объявляются при помощи типа, объекты - при помощи класса
Так же как может существовать много переменных одного и того же типа (например, строкового), не связанных между собой, возможно и наличие в программе множества объектов одного и того же класса, различающихся своими свойствами.
Например, мы можем рассматривать тип int как класс. Тогда переменная этого "класса" будет обладать одним-единственным свойством (ее целым значением), а также набором методов (сложение, вычитание, инкремент и т. д.). При этом методы выглядят как арифметические операторы+,-,++ и т. д.
В языке С++ мы могли бы, действительно, объявить новый тип int именно таким образом. Однако в РНР дело обстоит немного хуже: мы не имеем права переопределять стандартные операции (сложение, вычитание и т. д.) для объектов. Например, если бы мы захотели добавить в язык комплексные числа, в С++ это можно было сделать без особых затруднений (и класс комплексных чисел по использованию практически не отличался бы от встроенного типа int), однако в РНР нам такое добавление не удастся.
Альтернативное решение состоит в том, чтобы везде вместо + и других операций использовать вызовы соответствующих функций, например, add ( ) , которые бы являлись методами класса. Собственно, только такой способ организации методов и поддерживается в РНР (а также в Java).
Подход к созданию классов, применяемый в объектно-ориентированных языках, называют инкапсуляцией. Данные, принадлежащие классу, сохраняются в его свойствах, доступ к которым тщательно ограничивается и предоставляется в основном при помощи специальных методов.
Создание нового класса
Новый класс (тип данных) в программе описывается при помощи ключевого слова class. Внутри класса могут располагаться его свойства (переменные класса) и методы (функции-члены класса).
Давайте для тренировки опишем класс с именем мathComplex, объекты которого будут хранить комплексные числа (листинг 1). Этот класс пока поддерживает только сложение и вычитание чисел.
ПРИМЕЧАНИЕ
В математике комплексным числом называют пару двух вещественных чисел, первое из которых условно называют "действительной частью", а второе - "мнимой частью" комплексного числа. Все действительные числа соответствуют комплексным величинам с мнимой частью, равной нулю. Квадратный корень из -1, не существующий в виде действительного числа, имеет комплексное значение (0, 1), которое еще иногда обозначают знаком i. С комплексными числами можно выполнять все те же операции, что и с действительными - складывать, умножать, делить и т. д.
Листинг 1. Пример класс. Файл Math/Complex.php
Как видно из листинга 1, для объявления членов класса $re и $im мы воспользовались модификатором public, который более подробно будет освещен в следующих темах. "Добраться" до членов класса можно при помощи специальной переменной $this, которая всегда существует внутри методов (функций-членов) класса.
Файл, приведенный в листинге 1, при своем включении не выполняет никаких действий. Его задача - добавить в программу новый класс с именем мathComplex. В один файл можно добавлять множество классов, однако для облегчения поиска классов принято придерживаться рекомендации: один файл - один класс.
Работа с классами
Предположим, что в программе каким-то образом уже описан некоторый класс. Так как класс - это, по сути, тип данных, мы должны иметь некоторый механизм для создания переменных, хранящих значение этого типа.