Собственные типы в Pascal, такие как массивы и записи, позволяют программисту вводить новые абстракции. Их недостатками являются невозможность скрыть реализацию типов, ввести операции над ними и пр. В этом параграфе мы опишем правила объявления классов — основных типов пользователя в ООП, которые отчасти устраняют приведенные недостатки и, в этом смысле, гораздо больше напоминают стандартные встроенные типы языка.
Как и объявления других типов, объявления классов помещаются в разделе type. Объявление начинается с имени класса TPoint, затем следует знак равенства и ключевое слово class. Заканчивается объявление словом end.
Таким образом, объявления класса напоминает объявление записи. Fx и Fy как и в записи называются полями. Это элементы данных, которые будут индивидуальными у каждого объекта этого класса. А вот последующая часть объявления не встречалась в записях. В ней объявлены две процедуры и три функции. Процедуры и функции, включенные в описание класса, называются методами — методами работы с объектами данного класса. SetX, SetY позволяют задать значения координат точки, GetX, GetY — получить значение одной из координат, а Distance — установить расстояние от данной точки до точки p, переданной в качестве параметра.
Обратите внимание на имя класса — TPoint. Оно начинается с буквы T. Это один из элементов, так называемой венгерской нотации — соглашения об именах. T означает, что это имя типа. Fx означает, что это поле (field).
Приведенное объявление демонстрирует одну из сторон понятия инкапсуляции — элементы данных объединены в одно понятие со средствами работы с этими данными.
Где же описания алгоритмов методов нашего класса? Они должны располагаться в части implementation того же модуля, где объявлен класс и выглядят следующим образом:
В описаниях ко всем именам процедур и функций добавлено имя класса. Это позволяет компилятору различать методы разных классов, имеющих одинаковые имена. Никаких других различий между объявленными методами и заголовками в реализации не допускается. Должны совпадать даже имена формальных параметров.
Из примеров видно, что поля объекта не нужно передавать параметрами в методы. Обратите внимание на то, как в функции Distance выбираются поля своего объекта и объекта, переданного как параметр. Поля самого объекта используются непосредственно, тогда как для обращения к полям объекта–параметра, указывается имя объекта, а через точку имя поля, т.е. так же как для записи.
Одна из сложностей при переходе от процедурного программирования к ООП состоит в том, что программист должен понять схему создания и уничтожения объектов, в то время как создание и уничтожение обычных переменных продумано разработчиками языка, и о нем можно почти не задумываться.
Подчеркнем, что во всех случаях описание типа не приводит к созданию объекта данных этого типа. В Pascal переменные обычных типов создаются объявлением их имён в разделе var или путем передачи в процедуру в качестве параметра.
В ООП принято говорить не о переменных или данных, а об экземплярах (instances) объектов данного класса. Таким образом, следует различать объявление класса (типа) и создание экземпляров объектов этого класса. Аналогом экземпляров объектов являются переменные, но понятие объекта включает как данные, так и методы работы с ними. В дальнейшем, фраза типа “объект класса …” используется как синоним “экземпляр объекта класса …”, а иногда термин “объект данных” применяется и в отношении обычных переменных.
После разъяснений терминологии обратимся к процессу создания объектов. В отличие от обычных переменных, в Delphi Object Pascal при объявлении имени объекта в разделе var или передаче его в качестве параметра, создается не экземпляр объекта, а лишь ссылка на него. Сам объект еще предстоит создать и запомнить его адрес в отведенной ссылке, т.е. в Delphi все объекты динамические. Таким образом,
в Delphi все объекты динамические. Они автоматически не появляются и не исчезают. Созданием и уничтожением объектов управляет программист.
К детальному описанию процесса создания и уничтожения объектов в Delphi мы вернемся в разделе Конструкторы и Деструкторы, а до тех пор будем иметь ввиду упрощенную картину. В простейшем случае, для создания объекта данных достаточно выделить под него место в памяти. Это же нужно сделать и для объекта любого класса, а адрес выделенной области памяти нужно запомнить в ссылке на объект.
Выполняется выделение памяти специальным методом — конструктором Create, который имеется у каждого объекта, даже в тех случаях, когда он не описан среди методов. Откуда берется Create станет ясно позже. Вот как можно создать экземпляр точки:
Конструктор TPoint.Create выделил место в памяти и вернул указатель на выделенную область. Количество выделенной памяти в точности соответствует размеру объекта класса TPoint. Если по какой–либо причине создать объект не удастся, конструктор вернет значение Nil.
В Pascal в одних случаях переменные при создании инициализируются нулевыми значениями, а в других содержат случайный мусор. Что происходит при создании объектов? Оказывается, конструктор автоматически заполняет все поля нулевыми значениями.
После создания объекта можно выполнять различные действия над ним, например, задать значения координат
a.SetX(1); a.SetY(0);
Теперь координаты точки a равны (1, 0).
Обратите внимание на то, что при вызове конструктора, мы использовали имя класса, в то время как при вызове других методов — имя объекта. Это естественно, до создания объекта его методы вызвать нельзя, поэтому для вызова конструктора используется имя класса.
Вернемся к особенностям, связанным с динамическим характером объектов в Delphi. В a хранится ссылка на объект, но при использовании a значок ^ не ставится . В большинстве случаев это удобно — иначе пришлось бы для любого объекта указывать имя со шляпкой. С другой стороны, хотя шляпка отсутствует, программист обязан помнить, что a содержит лишь адрес, а не сам объект. Перечислим основные следствия из этого факта. Первое связано с операцией присваивания. Команда
b:=a;
приведет не к тому, что создастся объект b равный по значению a, а к тому, что на единственный объект, созданный при вызове конструктора будут указывать две ссылки a и b.
Второе следствие состоит в том, что даже если объект b уже создан, присваивание не приведет к копированию значений. Так после
экземпляр объекта, на который указывала ссылка b будет безвозвратно “утерян”, т.е. сам объект останется, но его адрес не сохранится. И снова, обе ссылки будут указывать на один и тот же объект. Правильный код для копирования значения в этом случае выглядит так
Сравнение a и b так же позволяет выяснить лишь, указывают ли обе ссылки на один и тот же объект.
Из того, что a является ссылкой, следует, что при передаче a в процедуру как фактического параметра, всегда будет передана ссылка на объект, т.е. все изменения будут выполняться над оригиналом объекта.
Наконец, если объект был объявлен и создан в процедуре, то после выхода из процедуры объект не исчезнет, а исчезнет лишь ссылка на него. Вообще, выделенная под объекты память автоматически освобождается только после завершения работы всей программы. В остальных случаях объекты уничтожает программист.
Для уничтожения каждый объект имеет стандартный метод Free и, так называемый, стандартный деструктор Destroy. Эти методы не имеют параметров и просто освобождают память. Метод Free, проверяет, не является ли указатель на объект нулевым и затем вызывает Destroy, поэтому он более безопасен и применять для уничтожения объектов следует именно его.
Объект опасно уничтожать дважды, как правило это приводит к ошибке. Если объекты уничтожаются в разных местах программы, то удобно одновременно с уничтожением устанавливать ссылку на объект в Nil, чтобы сообщить в остальные части программы, что объект уже отсутствует. Повторный вызов Free для такого объекта не будет иметь никаких последствий.
Вот как может выглядеть программа, вычисляющая расстояние между точками (1, 0) и (0, 1):
Подчеркнем, что a := Nil и b := Nil — не являются необходимыми, а просто мера предосторожности. |