Выбери формат для чтения
Загружаем конспект в формате doc
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
3. Объектно-ориентированное программирование (ООП) 3
3.1. Краеугольные камни ООП 3
3.1.1. Что такое объект? 3
3.1.2. Концептуальный пример объекта 3
3.1.3. Природа объекта 3
3.1.4. Понятие класса объектов 4
3.1.5. Три кита ООП 4
3.1.6. Объекты и компоненты 5
3.2. Классы 5
3.2.1. Понятие класса 5
3.2.2. Классы в программных модулях 7
3.3. Объекты 7
3.4. Методы 9
3.4.1. Понятие метода 9
3.4.2. Конструкторы и деструкторы 10
3.5. Свойства 11
3.5.1. Понятие свойства 11
3.5.2. Методы получения и установки значений свойств 12
3.5.3. Свойства-массивы 13
3.5.4. Свойство-массив как основное свойство объекта 14
3.5.5. Методы, обслуживающие несколько свойств 15
3.6. Наследование 15
3.6.1. Понятие наследования 15
3.6.2. Прародитель всех классов 16
3.6.3. Перекрытие атрибутов в наследниках 18
3.6.4. Совместимость объектов различных классов 19
3.6.5. Контроль и преобразование типов 19
3.7. Статические методы 19
3.7.1. Понятие статического метода 19
3.8. Виртуальные методы 20
3.8.1. Понятие виртуального метода 20
3.8.2. Механизм вызова виртуальных методов 20
3.8.3. Абстрактные виртуальные методы 21
3.8.4. Динамические методы 22
3.8.5. Методы обработки сообщений 22
3.9. Указатели на методы объектов 22
3.12. Метаклассы 24
3.12.1. Ссылки на классы 24
3.12.2. Методы классов 25
3.12.3. Виртуальные конструкторы 26
3.13. Классы общего назначения 26
3.13.1. Классы для представления списка строк 26
Свойства: 28
Методы: 28
События: 28
3.13.2. Классы для представления потока данных 28
Общие свойства: 29
Общие методы: 29
3.14. Итоги 30
4. Исключительные ситуации и надежное программирование 31
4.1. Ошибки и исключительные ситуации 31
4.2. Классы исключительных ситуаций 31
4.3. Обработка исключительных ситуаций 33
4.3.1. Создание исключительной ситуации 33
4.3.2. Распознавание класса исключительной ситуации 35
4.3.3. Пример обработки исключительной ситуации 35
4.3.4. Возобновление исключительной ситуации 36
4.3.5. Доступ к объекту, описывающему исключительную ситуацию 37
4.4. Защита выделенных ресурсов от пропадания 37
4.4.1. Утечка ресурсов и защита от нее 37
4.5. Итоги 39
3. Объектно-ориентированное программирование (ООП)
Delphi - это так называемый объектно-ориентированный (ОО) язык, при помощи которого вы можете заниматься объектно-ориентированным программированием (ООП). Такой стиль программирования очень отличается от процедурного программирования и может показаться немного странным для большинства программистов, не сталкивавшихся с ООП. Прежде всего, надо понять, что такое объект; именно на этом понятии базируется ООП.
3.1. Краеугольные камни ООП
3.1.1. Что такое объект?
Объект - это самостоятельный фрагмент кода, который знает о себе и может рассказать об этом другим объектам, если они зададут вопрос, который он понимает.
Объект имеет свойства и методы, являющиеся вопросами, на которые он может ответить (даже если они не выглядят вопросами). Набор методов, на которые объект знает как реагировать, является его интерфейсом. Некоторые методы являются общедоступными (public), это означает, что другой объект может вызвать (или активизировать) их. Этот набор методов известен под названием public-интерфейс.
Когда один объект вызывает метод другого объекта, это называется передачей сообщения. Эта фраза соответствует ОО-терминологии, но чаще всего люди говорят "Вызвать этот метод", а не "Передать это сообщение". В следующем разделе мы рассмотрим концептуальный пример, который должен прояснить все это.
3.1.2. Концептуальный пример объекта
Предположим, что мы имеем объект Человек. Каждый объект Человек имеет имя, возраст, национальность и пол. Каждый объект Человек знает, как говорить и ходить. Один объект может спросить у другого о его возрасте, или может cказать, чтобы другой объект начал (или закончил) перемещение. В терминах программирования вы можете создать объект Person и назначить ему некоторые переменные (например, имя и возраст). Если вы создали второй объект Person, он может спросить у первого его возраст или сказать ему начать перемещение. Он может сделать это путем вызова методов первого объекта Person.
Обычно концепция объекта остается неизменной и в языке Deplhi и в других объектно-ориентированных языках программирования, хотя реализуют они ее по-разному. Эта концепция универсальна. По этой причине объектно-ориентированные программисты, независимо от применяемого ими языка, общаются не так, как процедурные программисты. Процедурные программисты часто говорят о функциях и модулях. Объектно-ориентированные программисты говорят об объектах и часто говорят о них, используя личные местоимения. Вы часто можете услышать, как один ОО-программист говорит другому: "Этот объект Supervisor говорит здесь объекту Employee "Дай мне свой ID", поскольку он должен назначить задания для Employee".
Процедурные программисты могут считать такой способ мышления странным, но он является естественным для ОО-программистов. В их программном мире все является объектом (с некоторыми исключениями в языке Delphi), а программы представляют собой взаимодействие (или разговор) объектов между собой.
Объект = Данные + Операции
На основании этой формулы была разработана методология объектно-ориентированного программирования (ООП).
3.1.3. Природа объекта
В общем случае каждый объект "помнит" необходимую информацию, "умеет" выполнять некоторый набор действий и характеризуется набором свойств. То, что объект "помнит", хранится в его полях. То, что объект "умеет делать", реализуется в виде его внутренних процедур и функций, называемых методами. Свойства объектов аналогичны свойствам, которые мы наблюдаем у обычных предметов. Значения свойств можно устанавливать и читать. Программно свойства реализуются через поля и методы.
***Рисунок-представление объекта
Например, объект "кнопка" имеет свойство "цвет". Значение цвета кнопка запоминает в одном из своих полей. При изменении значения свойства "цвет" вызывается метод, который перерисовывает кнопку.
Кстати, этот пример позволяет сделать важный вывод: свойства имеют первостепенное значение для программиста, использующего объект. Чтобы понять суть и назначение объекта вы обязательно должны знать его свойства, иногда — методы, очень редко — поля (объект и сам знает, что с ними делать).
3.1.4. Понятие класса объектов
В объектных системах объекты одного типа принято объединять в классы объектов. Каждый объект всегда принадлежит некоторому классу объектов. Класс объектов — это обобщенное (абстрактное) описание множества однотипных объектов.
Объекты являются конкретными представителями своего класса, их принято называть экземплярами класса. Например, с точки зрения объектной технологии, каждый человеческий индивидуум – это экземпляр класса «Человек».
Класс объектов определяет свойства и методы, то есть тот интерфейс, который является единым для всех его экземпляров. Это определение устанавливает своего рода рамки, которые каждый экземпляр заполняет своими индивидуальными значениями. Стало быть, экземпляры имеют тип соответствующего класса.
Экземпляры одного класса могут отличаться лишь значениями своих свойств, но не своими методами. Методы устанавливаются для всех экземпляров при определении класса. Отсюда следует, что все члены одного класса обнаруживают не только идентичный интерфейс, но и идентичное «поведение».
В качестве примера представим себе класс «Треугольник», обладающий такими методами, как «Нарисовать», «Переместить», «Удалить», а также свойствами «Позиция», «Размер», «Цвет». Для этого класса определены методы и написан соответствующий код, с помощью которого треугольник рисуется, удаляется или перемещается с одной позиции на другую. Отдельные экземпляры обладают значениями свойств: определенной позицией, размером, цветом. Если требуется удалить какой-нибудь треугольник, пользуются его методом «Удалить». Хотя этот метод и определен в классе объектов, он является методом экземпляров, действующим для каждого экземпляра в отдельности, и относится непосредственно к тому треугольнику, для которого вызывается.
***Рисунок-класс и его экземпляры
3.1.5. Три кита ООП
Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме.
Объединение данных и операций в одну сущность — объект — тесно связано с понятием инкапсуляции. Инкапсуляция означает, что для внутренней реализации справедлив принцип сокрытия информации, согласно которому ни программный код, ни поля не выставляются на всеобщее обозрение и не могут использоваться за пределами объекта.
В противоположность этому объекты имеют открытый интерфейс, описываемый в виде набора свойств и методов. Методы определяют поведение объекта, а свойства – его «знания», иными словами, данные.
Второй кит ООП — наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют все унаследованные признаки, так и, возможно, новые.
Третий кит — это полиморфизм. Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Например, вы можете породить новый класс кнопок с рельефной надписью, переопределив метод рисования кнопки.
3.1.6. Объекты и компоненты
Когда прикладные программы были консольно-ориентированными, а пользовательский интерфейс был простым, объекты казались пределом развития программирования, поскольку были идеальным средством разбиения сложных задач на простые подзадачи. Однако с появлением графических систем программирование пользовательского интерфейса резко усложнилось. Программист в какой-то мере стал дизайнером, а визуальная компоновка и увязка элементов пользовательского интерфейса (кнопок, меток, строк редактора) начали отнимать основную часть времени. И тогда программистам пришла в голову идея визуализировать объекты, объединив программную часть объекта с его видимым представлением на экране дисплея в одно целое. То, что получилось в результате, было названо компонентом.
Компоненты в среде Delphi — это особые объекты, которые являются строительными кирпичиками визуальной среды разработки и приспособлены к визуальной установке свойств. Чтобы превратить объект в компонент, первый разрабатывается по определенным правилам, а затем помещается в палитру компонентов. Конструируя приложение, вы берете компоненты из Палитры Компонентов, располагаете на форме и устанавливаете их свойства в окне Инспектора Объектов. Внешне все выглядит просто, но чтобы достичь такой простоты, потребовалось создать механизмы, обеспечивающие функционирование объектов-компонентов уже на этапе проектирования приложения! Все это было придумано и блестяще реализовано в среде Delphi. Таким образом, компонентный подход значительно упростил создание приложений с графическим пользовательским интерфейсом и дал толчок развитию новой индустрии компонентов.
Cсейчас мы рассмотрим лишь вопросы создания и использования объектов, а потом научимся превращать объекты в компоненты.
3.2. Классы
3.2.1. Понятие класса
Для поддержки ООП в язык Delphi введены объектные типы данных, с помощью которых одновременно описываются данные и операции над ними. Объектные типы данных называют классами, а их экземпляры — объектами.
Классы объектов определяются в секции type глобального блока.
Описание класса начинается с ключевого слова class и заканчивается ключевым словом end. По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций. Такие процедуры и функции обобщенно называют методами, они предназначены для выполнения над объектами различных операций.
Общий синтаксис объявления класса:
type className = class (ancestorClass)
memberList
end;
MemberList служит для описания полей, методов и свойств.
Или:
Type
TClassName = class(TParentClass)
private
... { private declarations here}
protected
... { protected declarations here }
public
... { public declarations here }
published
... { published declarations here }
end;
Замечания:
1. Если TparentClass не указан, то подразумевается Tobject, включая базовые constructor и destructor.
2. Методы описываются своим заголовком без тела подпрограммы.
3. При описании полей, свойств и методов можно разграничить доступ к этим атрибутам класса специальных ключевых слов: private, protected, public, published.
• Private. Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации.
• Public. Поля, методы и свойства, объявленные в секции public не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public, служит для манипуляций с объектами и составляет программный интерфейс класса.
• Protected. Поля, методы и свойства, объявленные в секции protected, видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private, директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы.
• Published. Устанавливает правила видимости те же, что и директива public. Особенность состоит в том, что для элементов, помещенных в секцию published, компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO.
4. Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств.
5. Если в определении класса нет ключевых слов private, protected, public и published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published.
6. Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в этом же модуле, не действуют.
Приведем пример объявления класса:
TYPE
TCoordinates=record
X,Y:Integer;
end;
Tfigure = class
private
Fcolor:TColor;
Coords:TCoordinates;
Protected
Procedure setColor(c:TColor); virtual;
Procedure Draw; virtual; abstract;
Procedure Hide; virtual; abstract;
Procedure Move(NewX,NewY:Integer);
Property Color: TColor
read Fcolor
write setcolor;
End;
Класс содержит поля (Fcolor, Coords) и методы (Draw, Hide, Move). Заголовки методов, (всегда) следующие за списком полей, играют роль упреждающих (forward) описаний. Программный код методов пишется отдельно от определения класса.
В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на еще не определенный класс. Она решается с помощью упреждающего объявления:
type
TReadersList = class; // упреждающее объявление класса TReadersList
TDelimitedReader = class
Owner: TReadersList;
...
end;
TReadersList = class
Readers: array of TDelimitedReader;
...
end;
Первое объявление класса TDelimitedReader называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.
3.2.2. Классы в программных модулях
Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface, а код методов — в секцию implementation. Создавая модули классов, нужно придерживаться следующих правил:
• все классы, предназначенные для использования за пределами модуля, следует определять в секции interface;
• описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation;
• если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.
3.3. Объекты
Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var:
var
Figure: TFigure;
При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная Figure — это просто ссылка на экземпляр (объект в памяти), которого физически еще не существует.
Чтобы сконструировать объект (выделить память для экземпляра) класса TFigure и связать с ним переменную Figure, нужно в тексте программы поместить следующий оператор:
Figure := TFigure.Create;
Create — это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.
После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:
Figure.Move;
Кроме того, как и при работе с записями, допустимо использование оператора with, например:
with Figure do
Move;
Если объект становится ненужным, он должен быть удален вызовом специального метода Destroy, например:
Figure.Destroy; // Освобождение памяти, занимаемой объектом
Destroy — это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная Figure становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil. Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:
Figure := nil;
...
if Figure <> nil then Figure.Destroy;
Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке.
Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.
Figure.Free;
После уничтожения объекта переменная Figure сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:
Figure.Free;
Figure := nil;
С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:
FreeAndNil(Figure); // Эквивалентно: Figure.Free; Figure := nil;
Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:
var
R1, R2: TFigure; // Переменные R1 и R2 не связаны с объектом
begin
R1 := TFigure.Create; // Связывание переменной R1 с новым объектом
// Переменная R2 пока еще не связана ни с каким объектом
R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1
// Теперь обе переменные связаны с одним объектом
R2.Free; // Уничтожение объекта
// Теперь R1 и R2 не связаны ни с каким объектом
end;
Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.
Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.
3.4. Методы
3.4.1. Понятие метода
Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation.
Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем пример реализацию одного из методов в классе TFigure:
Procedure TFigure.Move (NewX,NewY:Integer);
Begin
Hide;
Coords.X:=NewX;
Coords.Y:=NewY;
Draw;
End;
Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self (стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод (Move) применяется.
Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.
3.4.2. Конструкторы и деструкторы
Особой разновидностью методов являются конструкторы и деструкторы. Напомним, что конструкторы создают, а деструкторы разрушают объекты. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение — очистку полей и освобождение памяти.
Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.
Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Для нашего класса TFigure потребуется конструктор, которому в качестве параметра будет передаваться начальная позиция объекта:
Type
TFigure = class
...
// Конструкторы и деструкторы
constructor Create(const Left,Top:integer);
destructor Destroy; override;
...
end;
Приведем их возможную реализацию:
constructor TFigure.Create(const Left,Top:integer);
begin
Coords.x:=left;
Coords.y:=top;
end;
destructor TFigure.Destroy;
begin
// Пока ничего не делаем
end;
Если объект содержит встроенные объекты или другие динамические данные, то конструктор — это как раз то место, где их нужно создавать.
Конструктор применяется к классу или к объекту. Если он применяется к классу,
Figure := TFigure.Create(100,200);
то выполняется следующая последовательность действий:
• в динамической памяти выделяется место для нового объекта;
• выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil;
• затем выполняются заданные программистом действия конструктора;
• ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TDelimitedReader).
Если конструктор применяется к объекту,
Figure.Create(100,200);
то конструктор выполняется как обычный метод. Другими словами, новый объект не создается, а происходит повторная инициализация полей существующего объекта. В этом случае конструктор не возвращает никакого значения. Далеко не все объекты корректно себя ведут при повторной инициализации, поскольку программисты редко закладывают такую возможность в свои классы. Поэтому на практике повторная инициализация применяется крайне редко.
Деструктор уничтожает объект, к которому применяется:
Figure.Destroy;
В результате:
• выполняется заданный программистом код завершения;
• освобождается занимаемая объектом динамическая память.
В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором.
Как и обычные методы, деструктор может иметь параметры, но эта возможность используется редко.
3.5. Свойства
3.5.1. Понятие свойства
Помимо полей и методов в объектах существуют свойства.
1. При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Создание сопутствующего эффекта достигается тем, что за присваиванием свойству значения стоит вызов метода.
2. Объявление свойства выполняется с помощью зарезервированного слова property:
property propertyName[indexes]: type index integerConstant specifiers;
Где:
• propertyName – любой корректный идентификатор;
• type – предопределенный или декларированный раньше тип;
• specifiers – спецификаторы доступа;
• [indexes] – необязательный параметр, который позволяет описать индексированное свойство (свойство-массив);
• index integerConstant – необязательная секция, содержащая ключевое слово index и некоторую константу типа Integer (передается автоматически как параметр для методов, указанных в спецификаторах доступа), используется для методов, обслуживающих несколько свойств.
Простейший пример:
Type
Tfigure = class
private
Fcolor:TColor;
...
Protected
//Метод записи (установки значения)свойства
Procedure setColor(c:TColor); virtual;
Property Color: TColor
read Fcolor
write setcolor; // Свойство
end;
3. При объявлении свойства используются ключевые слова: read, write, stored, default (nodefault) и implements, которые называются спецификаторами доступа. Каждое свойство должно обязательно содержать хотя бы один из спецификаторов: read, write.
• read – указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства. Например, чтение свойства Color означает чтение поля Fcolor.
• write — поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, установка свойства — вызов метода SetColor.
• stored –
4. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field).
5. Обращение к свойствам выглядит в программе как обращение к полям:
var
Fugure: TFigure;
...
Figure.Color := red; // Эквивалентно Figure.SetColor(red);
If Figure.Color = red then // Эквивалентно Figure.Fcolor;
ShowMessage(‘Красный’);
6. Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). В следующем примере объявлено свойство, значение которого можно только читать.
7. В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций.
8. Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолируя их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значений свойств; внешний интерфейс класса не изменится.
3.5.2. Методы получения и установки значений свойств
Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам.
Метод чтения свойства — это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства — это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта.
Пример:
Type
TFigure = class
FVisible: Boolean;
...
procedure SetVisible (const AVisible: Boolean);
...
property Visible: Boolean read FVisible write SetVisible;
end;
Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и т.д. Например, в методе SetVisible вполне целесообразно осуществить проверку состояния объекта (видим или невидим), чтобы избежать его повторного рисования или удаления с экрана:
procedure TFigure.SetVisible (const AVisible: Boolean);
begin
if Visible <> AVisible then // Если состояние изменяется
begin
if AVisible then
Draw; // Нарисовать
else
Hide; // Убрать с экрана
FVisible := AVisible; // Сохранение состояния в поле
end;
end;
Значение свойства может не храниться, а вычисляться при каждом обращении к свойству.
Пример:
TEllipse = class(TFigure) // эллипс
Private
FFocus:TCoordinates;
function GetFocus2: TCoordinates;
procedure SetFocus(const Value: TCoordinates);
Public
Property Focus1:TCoordinates
read FFocus
write SetFocus;
Property Focus2:TCoordinates
read GetFocus2;
end;
3.5.3. Свойства-массивы
1. Кроме обычных свойств в объектах существуют свойства-массивы (array properties). Свойство-массив — это индексированное множество значений. Например, в классе TPolygon множество вершин удобно представить в виде свойства-массива:
Type
TPolygon = class(TFigure)
...
FPoints: array of TCoordinates;
FPointsCount:word;
...
function GetPoints(Index: Integer): TCoordinates;
procedure SetPoints(Index: Integer; APoint:TCoordinates);
...
property Points[Index: Integer]: TCoordinates read GetPoints write SetPoints;
property PointsCount:word read FPointsCount write FPointsCount;
end;
...
function TPolygon.GetPoints(Index: Integer): TCoordinates;
begin
Result := FPoints[Index];
end;
procedure TPolygon.SetPoints(Index: Integer; APoints: TCoordinates);
begin
FPoints[Index]:=APoints;
end;
2. В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства.
3. Основная выгода от применения свойства-массива — возможность выполнения итераций с помощью цикла for, например:
Var
Polygon: TPolygon;
I: Integer;
...
for I := 0 to Polygon.PointsCount - 1 do
Polygon.Points[I].x:=100*I;
...
4. Свойство-массив может быть многомерным. В этом случае методы чтения и записи элементов должны иметь столько же индексных параметров соответствующих типов, что и свойство-массив.
5. Свойства-массивы имеют два важных отличия от обычных массивов:
их индексы не ограничиваются диапазоном и могут иметь любой тип данных, а не только Integer. Например, можно создать свойство-массив, в котором индексами будут строки. Обращение к такому свойству могло бы выглядеть примерно так:
Polygon.Points['First'].x := 100;
6. Операции над свойством-массивом в целом запрещены; разрешены операции только с его элементами.
3.5.4. Свойство-массив как основное свойство объекта
Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default.
Такое объявление свойства позволяет рассматривать сам объект класса как массив и опускать имя свойства-массива при обращении к нему из программы, например:
for I := 0 to Polygon.PointsCount - 1 do
Polygon.[I].x:=100*I;
Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.
3.5.5. Методы, обслуживающие несколько свойств
Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передается в метод чтения (записи) первым параметром.
Пример:
type
TRectangle = class(TFigure)
private
FCoordinates: array[0..3] of Longint;
function GetCoordinate(Index: Integer): Longint;
procedure SetCoordinate(Index: Integer; Value: Longint);
public
property Left: Longint index 0 read GetCoordinate write SetCoordinate;
property Top: Longint index 1 read GetCoordinate write SetCoordinate;
property Right: Longint index 2 read GetCoordinate write SetCoordinate;
property Bottom: Longint index 3 read GetCoordinate write SetCoordinate;
property Coordinates[Index: Integer]: Longint read GetCoordinate write SetCoordinate;
...
end;
Обращения к свойствам Left, Top, Right, Bottom и Coordinates заменяются компилятором на вызовы одного и того же метода GetCoordinate, но с разными значениями параметра Index:
Var
Rectangle: TRectangle;
...
ShowMessage(inttostr(Rectangle.Left));
//Эквивалентно:ShowMessage(inttostr(Rectangle.GetCoordinate(0)));
ShowMessage(inttostr(Rectangle.Top));
//Эквивалентно:ShowMessage(inttostr(Rectangle.GetCoordinate(1)));
ShowMessage(inttostr(Rectangle.Right));
//Эквивалентно:ShowMessage(inttostr(Rectangle.GetCoordinate(2)));
...
3.6. Наследование
3.6.1. Понятие наследования
Классы инкапсулируют (т.е. включают в себя) поля, методы и свойства; это их первая черта. Следующая не менее важная черта классов — способность наследовать поля, методы и свойства других классов.
Класс, который наследует атрибуты другого класса, называется порожденным классом или потомком. Соответственно класс, от которого происходит наследование, выступает в роли базового, или предка.
Чтобы пояснить сущность наследования обратимся к примеру. Раньше мы описывали классы Tfigure (с добавлением свойства Visible), Tpolygon, Tellipse.
TYPE
TCoordinates=record
X,Y:Integer;
end;
Tfigure = class
private
Fcolor:TColor;
Coords:TCoordinates;
Protected
Procedure setColor(c:TColor); virtual;
Procedure Draw; virtual; abstract;
Procedure Hide; virtual; abstract;
Procedure Move(NewX,NewY:Integer);
Property Color: TColor
read Fcolor
write setcolor;
End;
Рисунок дерева классов.
Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.
3.6.2. Прародитель всех классов
В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан.
Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.
Рисунок полного дерева классов.
Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:
type
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
Некоторые конструкции этого описания будут вам непонятны, поскольку мы их еще не изучали. Сейчас это не важно. Снова вернитесь к этому описанию после прочтения всей главы.
Краткое описание методов в классе TObject:
• Create — стандартный конструктор.
• Free — уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil.
• InitInstance(Instance: Pointer): TObject — при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.
• CleanupInstance — освобождает память, занимаемую полями с типом string, Variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.
• ClassType: TClass — возвращает описатель класса (метакласс).
• ClassName: ShortString — возвращает имя класса.
• ClassNameIs(const Name: string): Boolean — проверяет, является ли заданная строка именем класса.
• ClassParent: TClass — возвращает описатель базового класса.
• ClassInfo: Pointer — возвращает указатель на соответствующую классу таблицу RTTI (от англ. Runtime Type Information). Таблица RTTI используется для проверки типов данных на этапе выполнения программы.
• InstanceSize: Longint — возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Заметим, что значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передаче ей в качестве аргумента объектной переменной — это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная — это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize — это размер этих данных, а не размер объектной переменной.
• InheritsFrom(AClass: TClass): Boolean — проверяет, является ли класс AClass базовым классом.
• MethodAddress(const Name: ShortString): Pointer — возвращает адрес published-метода, имя которого задается параметром Name.
• MethodName(Address: Pointer): ShortString — возвращает имя published-метода по заданному адресу.
• FieldAddress(const Name: ShortString): Pointer — возвращает адрес published-поля, имя которого задается параметром Name.
• GetInterface(const IID: TGUID; out Obj): Boolean — возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6)
• GetInterfaceEntry(const IID: TGUID): PInterfaceEntry — возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.
• GetInterfaceTable: PInterfaceTable — возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.
• AfterConstruction — автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах).
• BeforeDestruction — автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).
• Dispatch(var Message) — служит для вызова методов, объявленных с ключевым словом message.
• DefaultHandler(var Message) — вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.
• NewInstance: TObject — вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
• FreeInstance — вызывается при уничтожении объекта для освобождения занятой объектом динамической памяти. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
• Destroy — стандартный деструктор.
3.6.3. Перекрытие атрибутов в наследниках
В механизме наследования можно условно выделить три основных момента:
• наследование полей;
• наследование свойств;
• наследование методов.
Наследование полей
1. Любой порожденный класс наследует от родительского все поля данных, поэтому классы Tpolygon и Tellipse автоматически содержат поля Fcolor и FVisible, объявленные в классе TFigure.
2. Доступ к полям предка осуществляется по имени, как если бы они были определены в потомке.
3. В потомках можно определять новые поля, но их имена должны отличаться от имен полей предка.
Наследование свойств и методов имеет свои особенности.
1. Свойство базового класса можно перекрыть (от англ. override) в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом.
2. Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Например, в классе Tellipse написать:
Procedure Draw; override;
Procedure Hide; override;
3. В наследнике можно вызвать перекрытый метод предка, указав перед именем метода зарезервированное слово inherited. Когда метод предка полностью совпадает с методом потомка по формату заголовка, то можно использовать более короткую запись. Например:
procedure Tellipse.Draw;
begin
inherited;
//свой код
end;
procedure Tellipse.Hide;
begin
inherited;
//Свой код
end;
4. В конструкторах сначала вызывается конструктор предка, а затем инициализируются дополнительные поля данных. В деструкторах применяется обратная последовательность действий: сначала разрушаются данные, недоступные предку, а затем вызывается унаследованный деструктор.
3.6.4. Совместимость объектов различных классов
Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Например, переменной типа TFigure можно присвоить значение переменной типа Tellipse:
var
Figure: TFigure;
...
Figure := TEllipse.Create;
Объектная переменная Figure формально имеет тип TFigure, а фактически связана с экземпляром класса TEllipse.
Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TFigure, то вместо него можно передать объект класса TEllipse или TPolygon.
Заметим, что все объекты являются представителями известного вам класса TObject. Поэтому любой объект любого класса можно использовать как объект класса TObject.
3.6.5. Контроль и преобразование типов
Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе.
В языке Delphi существуют операторы is и as, с помощью которых выполняется соответственно проверка на тип (type checking) и преобразование к типу (type casting).
Например, чтобы выяснить, принадлежит ли некоторый объект Obj к классу TFigure или его наследнику, следует использовать оператор is:
var
Obj: TObject;
...
if Obj is TFigure then ...
Для преобразования объекта к нужному типу используется оператор as, например
with Obj as TFigure do
Color:= red;
Стоит отметить, что для объектов применим и обычный способ приведения типа:
with TFigure(Obj) do
Color := Red;
Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию при выполнении программы (run-time error)), если реальный экземпляр объекта Obj не совместим с классом TFigure.
3.7. Статические методы
3.7.1. Понятие статического метода
По умолчанию, методы являются статическими. Пример описания:
Type
TFigure = class
procedure Draw;
end;
TRectangle = class(TFigure)
procedure Draw;
end;
При обращении к статическому методу компилятору (то есть уже на этапе компиляции) известен класс, которому данный метод принадлежит. Следовательно, при помощи статических методов невозможно реализовать принцип полиморфизма. Пусть, например, имеется некоторая процедура, которая принимает в качестве параметра и базовый класс и классы-потомки. В качестве примера рассмотрим SetVisible. Класс объекта, к которому применяется эта процедура, становится известен этапе выполнения. И хотелось, чтобы и метод Draw был не базового класса, в котором описана процедура SetVisible, а каждый раз своего класса. Иначе рисовка фигурки не будет корректной. В этом случае метод Draw должен быть перекрыт в потомках.
3.8. Виртуальные методы
3.8.1. Понятие виртуального метода
Виртуальные методы, в отличие от статических могут быть перекрыты в классах-потомках.
1. Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual, а его перекрытие в производных классах — с помощью ключевого слова override.
2. Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций еще и тип возвращаемого значения), что и перекрываемый:
Type
TFigure = class
procedure Draw; virtual;
end;
TRectangle = class(TFigure)
procedure Draw; override;
end;
TEllipse = class(TFigure)
procedure Draw; override;
end;
Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе.
var
Figure: TFigure;
rectangle: Trectangle;
Ellipse: Tellipse;
begin
Ellipse.visible:=true; //Draw, Hide от TEllipse
Rectangle.visible:=false; //Draw, Hide от TRectangle
end;
Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.
3.8.2. Механизм вызова виртуальных методов
Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Вы уже сталкивались с косвенным вызовом при использовании процедурных переменных. Процедурная переменная и была тем местом в памяти, где хранился адрес вызываемой подпрограммы. Для каждого виртуального метода тоже создается процедурная переменная, но ее наличие и использование скрыто от программиста.
Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT — от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.
Структуру объекта в оперативной памяти поясняет рисунок 3.3:
Рисунок 3.3. Структура объекта TTextReader в оперативной памяти
Вызов виртуального метода осуществляется следующим образом:
1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти;
2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);
3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;
4. Вызывается код, находящийся по этому адресу.
3.8.3. Абстрактные виртуальные методы
При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом Draw, тело которого в классе TFigure объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract:
Type
TFigure = class
procedure Draw; virtual; abstract;
end;
Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.
3.8.4. Динамические методы
Разновидностью виртуальных методов являются так называемые динамические методы.
1. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic.
2. В наследниках динамические методы перекрываются так же, как и виртуальные — с помощью зарезервированного слова override.
3. По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Методы, объявленные с директивой virtual, вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии.
4. Методы, объявленные с директивой dynamic вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Таким образом, программисту предоставляются два способа оптимизации объектов: по скорости работы (virtual) или по объему памяти (dynamic).
3.8.5. Методы обработки сообщений
Специализированной формой динамических методов являются методы обработки сообщений. Они объявляются с помощью ключевого слова message, за которым следует целочисленная константа — номер сообщения. Следующий пример взят из исходных текстов библиотеки VCL:
type
TWidgetControl = class(TControl)
...
procedure CMKeyDown(var Msg: TCMKeyDown); message CM_KEYDOWN;
...
end;
Метод обработки сообщений имеет формат процедуры и содержит единственный var-параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).
Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ.
3.9. Указатели на методы объектов
Мы уже говорили о процедурном типе. Переменная процедурного типа – это указатель на адрес процедуры или функции. Если же мы хотим сделать ссылку не просто на процедуру или функцию, а на метод объекта, то мы должны добавить ключевое словосочетание of object, записанное после прототипа процедуры или функции:
type
TMethod = procedure of object;
TNotifyEvent = procedure(Sender: TObject) of object;
Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода (получается, на самом деле 2 указателя). Например:
type
TNotifyEvent = procedure(Sender: TObject) of object;
TMainForm = class(TForm)
procedure ButtonClick(Sender: TObject);
...
end;
var
MainForm: TMainForm;
OnClick: TNotifyEvent
Тогда можно сделать следующую привязку:
OnClick := MainForm.ButtonClick;
Используя переменные типа указателя на метод можно описывать события. Рассмотрим пример с использованием ранее описанного класса TFigure.
Type
TFigure = class
private
FOnDraw: TNotifyEvent;
public
property OnDraw: TNotifyEvent read FOnDraw write FOnDraw;
end;
и в методе Draw:
procedure TFigure.Draw;
begin
... //рисуем фигурку
if Assigned(FOnDraw) then
FOnDraw(Self); //уведомление о рисовании фигурки
end;
end;
Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil. Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.
Описанный выше механизм называется делегированием, поскольку он позволяет передать часть работы другому объекту, например, сосредоточить в одном объекте обработку событий, возникающих в других объектах. Это избавляет программиста от необходимости порождать многочисленные классы-наследники и перекрывать в них виртуальные методы. Делегирование широко применяется в среде Delphi. Например, все компоненты делегируют обработку своих событий той форме, в которую они помещены.
3.12. Метаклассы
3.12.1. Ссылки на классы
Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса; его принято обозначать термином метакласс.
Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of, например:
type
TTextReaderClass = class of TTextReader;
Переменная типа TTextReaderClass объявляется в программе обычным образом:
var
ClassRef: TTextReaderClass;
Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:
ClassRef := TTextReader;
ClassRef := TDelimitedReader;
ClassRef := TFixedReader;
По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:
type
TClass = class of TObject;
Переменная типа TClass может ссылаться на любой класс.
Практическая ценность ссылок на классы состоит в возможности создавать программные модули, работающие с любыми классами объектов, даже теми, которые еще не разработаны.
Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 3.4.
Рисунок 3.4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти
3.12.2. Методы классов
Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class, например:
type
TTextReader = class
...
class function GetClassName: string;
end;
Передаваемый в метод класса неявный параметр Self содержит не ссылку на объект, а ссылку на класс, поэтому в теле метода нельзя обращаться к полям, методам и свойствам объекта. Зато можно вызывать другие методы класса, например:
class function TTextReader.GetClassName: string;
begin
Result := ClassName;
end;
Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.
Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:
var
Reader: TTextReader;
S: string;
begin
// Вызов метода с помощью ссылки на класс
S := TTextReader.GetClassName; // S получит значение 'TTextReader'
// Создание объекта класса TDelimitedReader
Reader := TDelimitedReader.Create('MyData.del');
// Вызов метода с помощью ссылки на объект
S := Reader.GetClassName; // S получит значение 'TDelimitedReader'
end.
Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.
3.12.3. Виртуальные конструкторы
Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual. Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.
var
P: TComponent;
T: TComponentClass; // TComponentClass = class of TComponent;
...
T := FindClass(ReadStr);
P := T.Create(nil);
...
На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.
3.13. Классы общего назначения
Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу "в общем виде", т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими "абстрактными" структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.
3.13.1. Классы для представления списка строк
Для работы со списками строк служат классы TStrings и TStringList. Они используются в библиотеке VCL повсеместно и имеют гораздо большую универсальность, чем та, что можно почерпнуть из их названия. Классы TStrings и TStringList служат для представления не просто списка строк, а списка элементов, каждый из которых представляет собой пару строка-объект. Если со строками не ассоциированы объекты, получается обычный список строк.
Класс TStrings используется визуальными компонентами и является абстрактным. Он не имеет собственных средств хранения строк и определяет лишь интерфейс для работы с элементами. Класс TStringList является наследником TStrings и служит для организации списков строк, которые используются отдельно от управляющих элементов. Объекты TStringList хранят строки и объекты в динамической памяти.
Свойства класса TStrings описаны ниже.
• Count: Integer — число элементов в списке.
• Strings[Index: Integer]: string — обеспечивает доступ к массиву строк по индексу. Первая строка имеет индекс, равный 0. Свойство Strings является основным свойством объекта.
• Objects[Index: Integer]: TObject — обеспечивает доступ к массиву объектов. Свойства Strings и Objects позволяют использовать объект TStrings как хранилище строк и ассоциированных с ними объектов произвольных классов.
• Text: string — позволяет интерпретировать список строк, как одну большую строку, в которой элементы разделены символами #13#10 (возврат каретки и перевод строки).
Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.
• Names[Index: Integer]: string — обеспечивает доступ к той части строки, в которой содержится имя.
• Values[const Name: string]: string — обеспечивает доступ к той части строки, в которой содержится значение. Указывая вместо Name ту часть строки, которая находится слева от знака равенства, вы получаете ту часть, что находится справа.
Управление элементами списка осуществляется с помощью следующих методов:
• Add(const S: string): Integer — добавляет новую строку S в список и возвращает ее позицию. Новая строка добавляется в конец списка.
• AddObject(const S: string; AObject: TObject): Integer — добавляет в список строку S и ассоциированный с ней объект AObject. Возвращает индекс пары строка-объект.
• AddStrings(Strings: TStrings) — добавляет группу строк в существующий список.
• Append(const S: string) — делает то же, что и Add, но не возвращает значения.
• Clear — удаляет из списка все элементы.
• Delete(Index: Integer) — удаляет строку и ассоциированный с ней объект. Метод Delete, также как метод Clear не разрушают объектов, т.е. не вызывают у них деструктор. Об этом вы должны позаботиться сами.
• Equals(Strings: TStrings): Boolean — Возвращает True, если список строк в точности равен тому, что передан в параметре Strings.
• Exchange(Index1, Index2: Integer) — меняет два элемента местами.
• GetText: PChar — возвращает все строки списка в виде одной большой нуль-терминированной строки.
• IndexOf(const S: string): Integer — возвращает позицию строки S в списке. Если заданная строка в списке отсутствует, функция возвращает значение -1.
• IndexOfName(const Name: string): Integer — возвращает позицию строки, которая имеет вид Имя=Значение и содержит в себе Имя, равное Name.
• IndexOfObject(AObject: TObject): Integer — возвращает позицию объекта AObject в массиве Objects. Если заданный объект в списке отсутствует, функция возвращает значение -1.
• Insert(Index: Integer; const S: string) — вставляет в список строку S в позицию Index.
• InsertObject(Index: Integer; const S: string; AObject: TObject) — вставляет в список строку S и ассоциированный с ней объект AObject в позицию Index.
• LoadFromFile(const FileName: string) — загружает строки списка из текстового файла.
• LoadFromStream(Stream: TStream) — загружает строки списка из потока данных (см. ниже).
• Move(CurIndex, NewIndex: Integer) — изменяет позицию элемента (пары строка-объект) в списке.
• SaveToFile(const FileName: string) — сохраняет строки списка в текстовом файле.
• SaveToStream(Stream: TStream) — сохраняет строки списка в потоке данных.
• SetText(Text: PChar) — загружает строки списка из одной большой нуль-терминированной строки.
Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.
Свойства:
• Duplicates: TDuplicates — определяет, разрешено ли использовать дублированные строки в списке. Свойство может принимать следующие значения: dupIgnore (дубликаты игнорируются), dupAccept (дубликаты разрешены), dupError (дубликаты запрещены, попытка добавить в список дубликат вызывает ошибку).
• Sorted: Boolean — если имеет значение True, то строки автоматически сортируются в алфавитном порядке.
Методы:
• Find(const S: string; var Index: Integer): Boolean — выполняет поиск строки S в списке строк. Если строка найдена, Find помещает ее позицию в переменную, переданную в параметре Index, и возвращает True.
• Sort — сортирует строки в алфавитном порядке.
События:
• OnChange: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChange генерируется после того, как были сделаны изменения.
• OnChanging: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChanging генерируется перед тем, как будут сделаны изменения.
Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:
var
Items: TStrings;
I: Integer;
begin
// Создание списка
Items := TStringList.Create;
Items.Add('Туризм');
Items.Add('Наука');
Items.Insert(1, 'Бизнес');
...
// Работа со списком
for I := 0 to Items.Count - 1 do
Items[I] := UpperCase(Items[I]);
...
// Удаление списка
Items.Free;
end;
3.13.2. Классы для представления потока данных
В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками. Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 3.1).
Класс
Описание
TStream
Абстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.
THandleStream
Поток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.
TFileStream
Поток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.
TMemoryStream
Поток, который хранит свои данные в оперативной памяти. Моделирует работу с файлом. Используется для хранения промежуточных результатов, когда файловый поток не подходит из-за низкой скорости передачи данных.
TResourceStream
Поток, обеспечивающий доступ к ресурсам в Windows-приложении.
TBlobStream
Обеспечивает последовательный доступ к большим полям таблиц в базах данных.
Таблица 3.1. Классы потоков
Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.
Общие свойства:
• Position: Longint — текущая позиция чтения-записи.
• Size: Longint — текущий размер потока в байтах.
Общие методы:
• CopyFrom(Source: TStream; Count: Longint): Longint — копирует Count байт из потока Source в свой поток.
• Read(var Buffer; Count: Longint): Longint — читает Count байт из потока в буфер Buffer, продвигает текущую позицию на Count байт вперед и возвращает число прочитанных байт. Если значение функции меньше значения Count, то в результате чтения был достигнут конец потока.
• ReadBuffer(var Buffer; Count: Longint) — читает из потока Count байт в буфер Buffer и продвигает текущую позицию на Count байт вперед. Если выполняется попытка чтения за концом потока, то генерируется ошибка.
• Seek(Offset: Longint; Origin: Word): Longint — продвигает текущую позицию в потоке на Offset байт относительно позиции, заданной параметром Origin. Параметр Origin может иметь одно из следующих значений: 0 — смещение задается относительно начала потока; 1 — смещение задается относительно текущей позиции в потоке; 2 — смещение задается относительно конца потока.
• Write(const Buffer; Count: Longint): Longint — записывает в поток Count байт из буфера Buffer, продвигает текущую позицию на Count байт вперед и возвращает реально записанное количество байт. Если значение функции отличается от значения Count, то при записи была ошибка.
• WriteBuffer(const Buffer; Count: Longint) — записывает в поток Count байт из буфера Buffer и продвигает текущую позицию на Count байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.
Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:
var
Stream: TStream;
S: AnsiString;
StrLen: Integer;
begin
// Создание файлового потока
Stream := TFileStream.Create('Sample.Dat', fmCreate);
...
// Запись в поток некоторой строки
StrLen := Length(S) * SizeOf(Char);
Stream.Write(StrLen, SizeOf(Integer)); // запись длины строки
Stream.Write(S, StrLen); // запись символов строки
...
// Закрытие потока
Stream.Free;
end;
3.14. Итоги
Теперь для вас нет секретов в мире ООП. Вы на достаточно серьезном уровне познакомились с объектами и их свойствами; узнали, как объекты создаются, используются и уничтожаются. Если не все удалось запомнить сразу — не беда. Возвращайтесь к материалам главы по мере решения стоящих перед вами задач, и работа с объектами станет простой, естественной и даже приятной. Когда вы достигните понимания того, как работает один объект, то автоматически поймете, как работают все остальные. Теперь мы рассмотрим то, с чем вы встретитесь очень скоро — ошибки программирования
4. Исключительные ситуации и надежное программирование
4.1. Ошибки и исключительные ситуации
Мы должны отдавать себе отчет в том, что в любом работающем приложении могут происходить ошибки. Причины этих ошибок бывают разными. Некоторые из них носят субъективный характер и вызваны неграмотными действиями программиста. Но существуют и объективные ошибки, их нельзя избежать при проектировании программы, но можно обнаружить во время ее работы. Примеров таких ошибок сколько угодно: недостаточный объем свободной памяти, отсутствие файла на диске, выход значений исходных данных из допустимого диапазона и т.д.
Хорошая программа должна справляться со своими ошибками и работать дальше, не зацикливаясь и не зависая ни при каких обстоятельствах. Для обработки ошибок можно, конечно, пытаться использовать структуры вида if then Exit. Однако в этом случае ваш стройный и красивый алгоритм решения основной задачи обрастет уродливыми проверками так, что через неделю вы сами в нем не разберетесь. Из этой почти тупиковой ситуации среда Delphi предлагает простой и элегантный выход — механизм обработки исключительных ситуаций.
Исключительная ситуация (exception) — это прерывание нормального хода работы программы из-за невозможности правильно выполнить последующие действия.
Представим, что подпрограмма … (привести пример с чтением данных из TstringGrid в числовой массив). При обнаружении проблемы подпрограмма должна создать исключительную ситуацию — прервать нормальный ход своей работы и передать управление тем операторам, которые смогут обработать ошибку. Как правило, операторы обработки исключительных ситуаций находятся в одной из вызывающих подпрограмм.
Механизм обработки исключительных ситуаций лучше всего подходит для взаимодействия программы с библиотекой подпрограмм. Подпрограммы библиотеки обнаруживают ошибки, но в большинстве случаев не знают, как на них реагировать. Вызывающая программа, наоборот, знает, что делать при возникновении ошибок, но, как правило, не умеет их своевременно обнаруживать. Благодаря механизму обработки исключительных ситуаций обеспечивается связь между библиотекой и использующей ее программой при обработке ошибок.
Механизм обработки исключительных ситуаций довольно сложен в своей реализации, но для программиста он прост и прозрачен. Для его использования в язык Delphi введены специальные конструкции try...except...end, try...finally...end и оператор raise.
4.2. Классы исключительных ситуаций
Исключительные ситуации в языке Delphi описываются классами. Каждый класс соответствует определенному типу исключительных ситуаций. Когда в программе возникает исключительная ситуация, создается объект соответствующего класса, который переносит информацию об этой ситуации из места возникновения в место обработки.
Классы исключительных ситуаций образуют иерархию, корнем которой является класс Exception. Класс Exception описывает самый общий тип исключительных ситуаций, а его наследники — конкретные виды таких ситуаций (см. таблицу). Например, класс EOutOfMemory порожден от Exception и описывает ситуацию, когда свободная оперативная память исчерпана.
В следующей таблице приведены стандартные классы исключительных ситуаций, объявленные в модуле SysUtils. Они покрывают практически весь спектр возможных ошибок. Если их все-таки окажется недостаточно, вы можете объявить новые классы исключительных ситуаций, порожденные от класса Exception или его наследников.
Класс исключительных ситуаций
Описание
EAbort
«Безмолвная» исключительная ситуация, используемая для выхода из нескольких уровней вложенных блоков или подпрограмм. При этом на экран не выдается никаких сообщений об ошибке. Для генерации исключительной ситуации класса EAbort нужно вызвать стандартную процедуру Abort.
EInOutError
Ошибка доступа к файлу или устройству ввода-вывода. Код ошибки содержится в поле ErrorCode.
EExternal
Исключительная ситуация, возникшая вне программы, например, в операционной системе.
EExternalException
Исключительная ситуация, возникшая за пределами программы, например в DLL-библиотеке, разработанной на языке C++.
EHeapException
Общий класс исключительных ситуаций, возникающих при работе с динамической памятью. Является базовым для классов EOutOfMemory и EInvalidPointer.Внимание! Создание исключительных ситуаций этого класса (и всех его потомков) полностью берет на себя среда Delphi, поэтому никогда не создавайте такие исключительные ситуации с помощью оператора raise.
EOutOfMemory
Свободная оперативная память исчерпана (см. EHeadException).
EInvalidPointer
Попытка освободить недействительный указатель (см. EHeadException). Обычно это означает, что указатель уже освобожден.
EIntError
Общий класс исключительных ситуаций целочисленной арифметики, от которого порождены классы EDivByZero, ERangeError и EIntOverflow.
EDivByZero
Попытка деления целого числа на нуль.
ERangeError
Выход за границы диапазона целого числа или результата целочисленного выражения.
EIntOverflow
Переполнение в результате целочисленной операции.
EMathError
Общий класс исключительных ситуаций вещественной математики, от которого порождены классы EInvalidOp, EZeroDivide, EOverflow и EUnderflow.
EInvalidOp
Неверный код операции вещественной математики.
EZeroDivide
Попытка деления вещественного числа на нуль.
EOverflow
Потеря старших разрядов вещественного числа в результате переполнения разрядной сетки.
EUnderflow
Потеря младших разрядов вещественного числа в результате переполнения разрядной сетки.
EInvalidCast
Неудачная попытка приведения объекта к другому классу с помощью оператора as.
EConvertError
Ошибка преобразования данных с помощью функций IntToStr, StrToInt, StrToFloat, StrToDateTime.
EVariantError
Невозможность преобразования варьируемой переменной из одного формата в другой.
EAccessViolation
Приложение осуществило доступ к неверному адресу в памяти. Обычно это означает, что программа обратилась за данными по неинициализированному указателю.
EPrivilege
Попытка выполнить привилегированную инструкцию процессора, на которую программа не имеет права.
EStackOverflow
Стек приложения не может быть больше увеличен.
EControlC
Во время работы консольного приложения пользователь нажал комбинацию клавиш Ctrl+C.
EAssertionFailed
Возникает при вызове процедуры Assert, когда первый параметр равен значению False.
EPackageError
Проблема во время загрузки и инициализации библиотеки компонентов.
EOSError
Исключительная ситуация, возникшая в операционной системе.
Таблица. Классы исключительных ситуаций
Наследование классов позволяет создавать семейства родственных исключительных ситуаций. Примером такого семейства являются классы исключительных ситуаций вещественной математики, которые объявлены в модуле SysUtils следующим образом.
type
EMathError = class(Exception);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
Класс исключительных ситуаций EMathError является базовым для классов EInvalidOp, EZeroDivide, EOverflow и EUnderflow, поэтому, обрабатывая исключительные ситуации класса EMathError, вы будете обрабатывать все ошибки вещественной математики, включая EInvalidOp, EZeroDivide, EOverflow и EUnderflow.
Нетрудно заметить, что имена классов исключений начинаются с буквы E (от слова Exception). Этого правила полезно придерживаться при объявлении собственных классов исключений, например:
type
EMyException = class(Exception)
MyErrorCode: Integer;
end;
Как описываются классы исключительных ситуаций понятно, рассмотрим теперь, как такие ситуации обрабатываются.
4.3. Обработка исключительных ситуаций
4.3.1. Создание исключительной ситуации
Идея обработки исключительных ситуаций состоит в следующем. Когда подпрограмма сталкивается с невозможностью выполнения последующих действий, она создает объект с описанием ошибки и прерывает нормальный ход своей работы с помощью оператора raise. Так возникает исключительная ситуация.
raise EOutOfMemory.Create('Маловато памяти');
Данный оператор создает объект класса EOutOfMemory (класс ошибок исчерпания памяти) и прерывает нормальное выполнение программы. Вызывающие подпрограммы могут эту исключительную ситуацию перехватить и обработать. Для этого в них организуется так называемый защищенный блок:
try
// защищаемые от ошибок операторы
except
// операторы обработки исключительной ситуации
end;
Между словами try и except помещаются защищаемые от ошибок операторы. Если при выполнении любого из этих операторов возникает исключительная ситуация, то управление передается операторам между словами except и end, образующим блок обработки исключительных ситуаций. При нормальном (безошибочном) выполнении программы блок except...end пропускается (рисунок 4.1).
Рисунок 4.1. Логика работы оператора try…except…end
При написании программы вы можете использовать вложенные защищенные блоки, чтобы организовать локальную и глобальную обработку исключительных ситуаций. Концептуально это выглядит следующим образом:
try
// защищаемые операторы
try
// защищаемые операторы
except
// локальная обработка исключительных ситуаций
end;
// защищаемые операторы
except
// глобальная обработка исключительных ситуаций
end;
Исключительные ситуации внешнего защищенного блока, возникающие за пределами вложенного блока, обрабатываются внешней секцией except...end. Исключительные ситуации вложенного защищенного блока обрабатываются вложенной секцией except...end.
4.3.2. Распознавание класса исключительной ситуации
Распознавание класса исключительной ситуации выполняется с помощью конструкций
on <класс исключительной ситуации> do <оператор>;
которые записываются в секции обработки исключительной ситуации, например:
try
// вычисления с вещественными числами
except
on EZeroDivide do ... ; // обработка ошибки деления на нуль
on EMathError do ... ; // обработка других ошибок вещественной математики
end;
Поиск соответствующего обработчика выполняется последовательно до тех пор, пока класс исключительной ситуации не окажется совместимым с классом, указанным в операторе on. Как только обработчик найден, выпоняется оператор, стоящий за словом do и управление передается за секцию except...end. Если исключительная ситуация не относится ни к одному из указанных классов, то управление передается во внешний блок try...except...end и обработчик ищется в нем.
Обратите внимание, что порядок операторов on имеет значение, поскольку распознавание исключительных ситуаций должно происходить от частных классов к общим классам, иначе говоря, от потомков к предкам. С чем это связано? Сейчас поймете. Представьте, к чему приведет изменение порядка операторов on в примере выше, если принять во внимание, что класс EMathError является базовым для EZeroDivide. Ответ простой: обработчик EMathError будет поглощать все ошибки вещественной математики, в том числе EZeroDivide, в результате обработчик EZeroDivide никогда не выполнится.
На самом высоком уровне программы бывает необходимо перехватывать все исключительные ситуации, чтобы в случае какой-нибудь неучтенной ошибки корректно завершить приложение. Для этого применяется так называемый обработчик по умолчанию (default exception handler). Он записывается в секции except после всех операторов on и начинается ключевым словом else:
try
{ вычисления с вещественными числами }
except
on EZeroDivide do { обработка ошибки деления на нуль };
on EMathError do { обработка других ошибок вещественной математики };
else { обработка всех остальных ошибок (обработчик по умолчанию) };
end;
Отсутствие части else соответствует записи else raise, которое нет смысла использовать явно. На мой взгляд нет необходимости пользоваться обработкой исключительных ситуаций по умолчанию, поскольку все ваши приложения будут строиться, как правило, на основе библиотеки VCL, в которой обработка по умолчанию уже предусмотрена.
4.3.3. Пример обработки исключительной ситуации
В качестве примера обработки исключительной ситуации рассмотрим две функции: StringToCardinal и StringToCardinalDef.
Функция StringToCardinal выполняет преобразование строки в число с типом Cardinal. Если преобразование невозможно, функция создает исключительную ситуацию класса EConvertError.
function StringToCardinal(const S: string): Cardinal;
var
I: Integer;
B: Cardinal;
begin
Result := 0;
B := 1;
for I := Length(S) downto 1 do
begin
if not (S[I] in ['0'..'9']) then
raise EConvertError.Create(S + ' is not a valid cardinal value');
Result := Result + B * (Ord(S[I]) - Ord('0'));
B := B * 10;
end;
end;
Функция StringToCardinalDef также выполняет преобразование строки в число с типом Cardinal, но в отличие от функции StringToCardinal она не создает исключительную ситуацию. Вместо этого она позволяет задать значение, которое возвращается в случае неудачной попытки преобразования:
function StringToCardinalDef(const S: string; Default: Cardinal = 0): Cardinal;
begin
try
Result := StringToCardinal(S);
except
on EConvertError do
Result := Default;
end;
end;
Для преобразования исходной строки в число используется определенная выше функция StringToCardinal. Если при преобразовании возникает исключительная ситуация, то она «поглощается» функцией StringToCardinalDef, которая в этом случае возвращает значение параметра Default. Если происходит какая-нибудь другая ошибка (не EConvertError), то управление передается внешнему блоку обработки исключительных ситуаций, из которого была вызвана функция StringToCardinalDef.
Пример очень прост, но хорошо демонстрирует преимущества исключительных ситуаций перед традиционной обработкой ошибок. Представьте более сложные вычисления, состоящие из множества операторов, в каждом из которых может произойти ошибка. Насколько сложной окажется обработка ошибок многочисленными операторами if и насколько простой оператором try.
4.3.4. Возобновление исключительной ситуации
В тех случаях, когда защищенный блок не может обработать исключительную ситуацию полностью, он выполняет только свою часть работы и возобновляет исключительную ситуацию с тем, чтобы ее обработку продолжил внешний защищенный блок:
try
// вычисления с вещественными числами
except
on EZeroDivide do
begin
// частичная обработка ошибки
raise; // возобновление исключительной ситуации
end;
end;
Если ни один из внешних защищенных блоков не обработал исключительную ситуацию, то управление передается стандартному обработчику исключительной ситуации, завершающему приложение.
4.3.5. Доступ к объекту, описывающему исключительную ситуацию
При обработке исключительной ситуации может потребоваться доступ к объекту, описывающему эту ситуацию и содержащему код ошибки, текстовое описание ошибки и т.д. В этом случае используется расширенная запись оператора on:
on <идентификатор объекта> : <класс исключительной ситуации> do <оператор>;
Например, объект исключительной ситуации нужен для того, чтобы выдать пользователю сообщение об ошибке:
try
// защищаемые операторы
except
on E: EOutOfMemory do
ShowMessage(E.Message);
end;
Переменная E — это объект исключительной ситуации, ShowMessage — процедура модуля DIALOGS, отображающая на экране небольшое окно с текстом и кнопкой OK. Свойство Message типа string определено в классе Exception, оно содержит текстовое описание ошибки. Исходное значение для текста сообщения указывается при конструировании объекта исключительной ситуации.
Обратите внимание, что после обработки исключительной ситуации освобождение соответствующего объекта выполняется автоматически, вам этого делать не надо.
4.4. Защита выделенных ресурсов от пропадания
4.4.1. Утечка ресурсов и защита от нее
Программы, построенные с использованием механизма исключительных ситуаций, обязаны придерживаться строгих правил распределения и освобождения таких ресурсов, как память, файлы, ресурсы операционной системы.
Представьте ситуацию: подпрограмма распределяет некоторый ресурс, но исключительная ситуация прерывает ее выполнение, и ресурс остается не освобожденным. Даже подумать страшно, к чему может привести такая ошибка: утечка памяти, файловых дескрипторов, других ресурсов операционной системы. Следовательно, ресурсы нуждаются в защите от исключительных ситуаций. Для этого в среде Delphi предусмотрен еще один вариант защищенного блока:
// запрос ресурса
try
// защищаемые операторы, которые используют ресурс
finally
// освобождение ресурса
end;
Особенность этого блока состоит в том, что секция finally...end выполняется всегда независимо от того, происходит исключительная ситуация или нет. Если какой-либо оператор секции try...finally генерирует исключительную ситуацию, то сначала выполняется секция finally...end, называемая секцией завершения (освобождения ресурсов), а затем управление передается внешнему защищенному блоку. Если все защищаемые операторы выполняются без ошибок, то секция завершения тоже работает, но управление передается следующему за ней оператору. Обратите внимание, что секция finally...end не обрабатывает исключительную ситуацию, в ней нет ни средств ее обнаружения, ни средств доступа к объекту исключительной ситуации.
Рисунок. Логика работы оператора try…finally…end
Блок try...finally...end обладает еще одной важной особенностью. Если он помещен в цикл, то вызов из защищенного блока процедуры Break с целью преждевременного выхода из цикла или процедуры Continue с целью перехода на следующую итерацию цикла сначала обеспечивает выполнение секции finally...end, а затем уже выполняется соответствующий переход. Это утверждение справедливо также и для процедуры Exit (выход из подпрограммы).
Как показывает практика, подпрограммы часто распределяют сразу несколько ресурсов и используют их вместе. В таких случаях применяются вложенные блоки try...finally...end:
// распределение первого ресурса
try
...
// распределение второго ресурса
try
// использование обоих ресурсов
finally
// освобождение второго ресурса
end;
...
finally
// освобождение первого ресурса
end;
Кроме того, вы успешно можете комбинировать блоки try...finally...end и try...except...end для защиты ресурсов и обработки исключительных ситуаций.
4.5. Итоги
В этой главе вы узнали многое об исключительных ситуациях и способах борьбы с ними. Теперь ваши программы наверняка дадут достойный отпор не только самому неотесанному пользователю, но и его деревянному компьютеру. Это, кстати говоря, одно из необходимых качеств, которые позволят отнести вашу программу к классу хороших. Позволим себе также напомнить, что здесь были рассмотрены только ошибки времени выполнения, поэтому не забудьте прочитать гл.10, где рассказано о борьбе с логическими ошибками.