Делфи конструктор и деструктор

Обновлено: 14.05.2024

Конструкторы и деструкторы отвечают за существование объекта в памяти, т.е. выделяют память для экземпляра класса, затем и освобождают ее.

Конструктор — это специальный вид подпрограммы, присоединенный к классу. Его на-значение — создавать представителей (экземпляры) класса. Он ведет себя как функция, которая возвращает ссылку на вновь созданный экземпляр класса, т.е. на объект. Одновременно выделяется память для хранения значений полей экземпляра класса.

Деструктор — это специальная разновидность подпрограммы, присоединенной к классу. Его назначение заключается в уничтожении экземпляра класса, т.е. объекта и освобождении памяти, выделенной под экземпляр.

Синтаксис объявления конструкторов и деструкторов:

Type
=Сlass[)]
. . .
Constructor Имя конструктора>[( )]; [Override;]
Destructor [( )>; [Override;]
End;

Примечания:
• Объявляются конструкторы и деструкторы, как правило, в разделе Public класса.
• В классе может быть объявлено несколько конструкторов, однако чаще бывает один конструктор. Общепринятое имя для единственного конструктора Create.
• В одном классе может быть объявлено несколько деструкторов, но чаще бывает один деструктор без параметров (всегда!) с именем Destroy.
• За объявлением деструктора по имени Destroy следует указывать ключевое слово-директиву Override, разрешающее выполнение предусмотренных по умолчанию действий для уничтожения экземпляра объекта, если при его создании возникла какая-либо ошибка. Фактически Override переопределяет метод предка
• Метод Free так же удаляет (разрушает) экземпляры класса, предварительно проверяя их на Nil.

Реализация конструкторов

В задачу конструктора входит создание экземпляра класса и выполнение операторов, содержащихся в его теле. Назначение кода внутри конструктора — инициализировать только что созданный экземпляр объекта. Синтаксис реализации конструктора:

Constructor . [( >>;
[ >
Begin

End;

Реализация наследуемых конструкторов.

Constructor . [( )];
[ >
Begin
Inherited [( )>;

End;

Примечания:
• Следует убедиться, что для каждого поля в конструкторе предусмотрен оператор присвоения, и что все поля переходят из неопределенного состояния в какое-то конкретное, предусмотренное по умолчанию, хотя и известно, что транслятор сам выполнит инициализацию нулевыми значениями (», Nil, False — для других полей). Для неклассовых полей такая инициализация вполне допустима, поскольку позволяет их использовать в дальнейшем.
• Для вызова наследуемого конструктора следует использовать ключевое слово Inherited, которое фактически обеспечивает доступность перекрытого метода. Сила оператора в том, что он вызывает старое, а затем возвращается к новому.
• Как правило, следует вызывать подходящий наследуемый конструктор в первом исполняемом операторе.
• Только, если у пользовательского класса нет новых полей, можно не создавать для него конструктор.
• Хотя в объявлениях конструкторов и не указывается тип возвращаемого результата, и они выглядят, как объявления процедур, конструкторы используются скорее как функции, а не как процедуры. Можно сказать, что конструктор является неявной функцией — он возвращает нового представителя того класса, который использовался при его вызове. Нет необходимости в явном виде задавать тип возвращаемого результата, поскольку этот тип и так известен на момент вызова — это тип использованного в вызове конструктора класса.
• Внутри конструктора отсутствует и явное присвоение возвращаемого значения. Такое возможно, поскольку он всегда возвращает ссылку (адрес) на вновь созданный и инициализированный экземпляр класса.
• Начиная с класса TComponent конструктор Create стал виртуальным и при его переопределении необходимо указывать слово-директиву Override.

Реализация деструкторов

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

Синтаксис реализации деструктора:

Destructor . [( )];
[ ]
Begin

End;

Реализация наследуемых деструкторов
Если использовать механизм наследования деструкторов, то можно упростить задачу уничтожения экземпляров класса, таким образом, чтобы каждый раз заботиться лишь об уничтожении тех полей, которые были добавлены в данном классе. Всю работу по очистке наследуемых полей можно возложить на наследуемые деструкторы. Для вызова наследуемого деструктора необходимо используется ключевое слово Inherited.

Синтаксис объявления наследуемого деструктора следующий:

Destructor . [( )>;
[ ]
Begin

Inherited [ < )];
End;

Примечания:
• Внутри деструктора есть доступ не только к обычным идентификаторам, но и к полям экземпляра класса, инкапсулированным при его определении.
• Исполняемые операторы деструктора должны позаботиться обо всех операциях очистки, необходимых для уничтожения экземпляра класса. Код деструктора должен уничтожить все внутренние экземпляры объектов и освободить динамическую память, которая была зарезервирована во время существования экземпляра класса. Однако нет необходимости явно устанавливать в нулевые значения поля прямого доступа.
• Необходимость в объявлении деструкторов с параметрами возникает очень редко. Обычно все, что нужно сделать деструктору — уничтожить экземпляр класса, и вся необходимая для этого информация и так доступна ему.
• Если у класса нет полей косвенного доступа, то деструктор можно не создавать для такого пользовательского класса.

Вызов конструкторов

Для того чтобы вызвать конструктор, необходимо правильно объявить и определить класс. Объявление класса должно быть доступно, т.е. он должен находится в области видимости из того места, где конструктор будет вызываться.

Если в пользовательском классе не определен конструктор, то по умолчанию будет использоваться конструктор, унаследованный от класса-потомка. В любом случае у всех объектов есть доступ к конструктору Create, определенному в классе TObject.

Определение класса создает активную структуру, способную создавать представителей этого класса. Объекты, которые создаются с помощью определения класса, способны хранить ссылки на вновь создаваемые объекты.

Var : ; // Объявление переменной -указателя
Begin
:= . [( )];

• Если вызвать конструктор от имени объекта, то новый объект не будет создан (память не выделяется), но будут выполнены операторы, указанные в коде конструктора.
• Конструктор может также вызываться с помощью переменной типа указателя на класс.

Вызов деструкторов

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

Синтаксис вызова конструктора следующий:

Примечания:
• После вызова деструктора объект становится недоступен. Целесообразно присваивать объекту значение Nil сразу после его уничтожения, чтобы в дальнейшем можно было бы проверить его существование.
• Не следует вызывать деструкторы непосредственно. Вызов метода Free, наследуемого от TObject.Free, сравнивает указатель экземпляра со значением Nil перед тем, как вызвать деструктор Destroy.

Вызовы конструкторов и деструкторов визуальных компонентов Delphi. Любой компонент, попавший в ваше приложение при визуальном проектировании, включается в определенную иерархию объектов, которая замыкается на форме (класс TFonn). Поэтому вызов конструкторов и деструкторов всех компонентов формы производится автоматически при инициализации и удалении формы, незримо для программиста.

Сами формы создаются и уничтожаются приложением — глобальным объектом с именем Application. В файле-проекте с расширением *.Dpr можно увидеть вызов конструктора формы в виде строки:

Application.CreateForm(TForml, Fonnl);

Динамическое создание объектов. Пользователь может создать объект и программным путем:

Var Mem: TMemo;
Begin
Mem:=TMemo.Create(Self); // Создание экземпляра класса TMemo
Mem.Parent:=Self; // Form1 указывать не обязательно
Mem.Name:=’TmpMem’; // Присвоение имени компоненту
FindComponent(‘TmpMem’).Free; // Удаление компонента

Конструкторы - это специальные методы, создающие и инициализирующие объект класса. Объект создается выделением для него памяти в динамически распределяемой области памяти heap. Объявление конструктора выглядит так же, как объявление процедуры, но предваряется ключевым словом constructor. В качестве имени конструктора обычно задают имя Create.

В реализации конструктора обычно первым идет вызов наследуемого конструктора с помощью ключевого слова inherited. В результате инициализируются все наследуемые поля. При этом порядковым типам в качестве начального значения задается 0, указателям — nil, строки задаются пустыми. После вызова наследуемого конструктора в процедуре инициализируются новые поля, введенные в данном классе.

Не обязательно при создании класса объявлять его конструктор. Если он не объявлен, то автоматически в момент создания объекта вызовется конструктор родительского класса. Мы это видели в примере класса TPerson и его тестового приложения, созданных во второй части. Там мы обошлись без конструктора, и все работало нормально. Но давайте несколько расширим возможности нашего класса, и посмотрим, сможем ли мы обойтись без конструктора. Добавим в класс открытые поля AgeMin, AgeMax, обозначающие минимальную и максимальную границы допустимого возраста. Подобные границы полезны, если приложение использует наш класс для регистрации претендентов на какую-то должность при приеме на работу, или для регистрации абитуриентов при приеме в учебное заведение. Пусть при задании года рождения класс автоматически проверяет, удовлетворяет ли регистрируемая личность поставленным возрастным ограничениям. Для реализации этой идеи в объявлении класса надо произвести следующие изменения:

В оператор uses вводится ссылка на модуль DateUtils, В этом модуле объявлены функции, которые мы будем использовать при записи года рождения. В защищенный раздел класса protected вводится объявление процедуры записи года рождения. В открытый раздел класса вводятся переменные AgeMin и AgeMax. И изменяется объявление свойства Year: теперь в него вводится ссылка на процедуру записи SetYear.
Реализация процедуры записи SetYear может быть следующей:

В переменную NowYear заносится текущий год. Делается это так. Вызывается функция Date, которая возвращает текущую дату. Затем к этому результату применяется функция YearOf, которая выделяет из даты год. Функция YearOf объявлена в модуле DateUtils. Именно из-за нее этот модуль подключается предложением uses.

Возможно, вы уже заметили, почему все это пока не будет нормально работать. Если нет, введите описанные изменения в ваш класс и выполните тестовое приложение. Вы увидите, что можете занести в объект класса только того, кто родился в текущем году. А такой гражданин, пожалуй, слишком молод для приема на работу или учебу. Объясняется это просто: целые поля, такие как AgeMin и AgeMax, инициализируются нулевыми значениями. А нам нужны какие-то другие, более разумные возрастные рамки. Конечно, пользователь класса легко может этот недостаток устранить. Достаточно ввести, например, в обработчик события формы OnCreate после оператора создания объекта класса операторы вида:

Pers.AgeMax := 45;
Pers.AgeMin := 16;

Тем самым пользователь задает возрастные рамки для своего приложения. Ведь открытые поля AgeMin и AgeMax мы для того и вводили. Но хотелось бы, чтобы в случаях, если пользователю не требуется какой-то специфический возрастной диапазон, класс позволял бы работать с любыми реальными годами рождения. Но именно реальными, чтобы предотвратить случайную ошибку пользователя, когда он задаст, например, год рождения 3 или 3003.

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

constructor Create;

А в раздел implementation добавьте реализацию конструктора:

Первый оператор вызывает с помощью ключевого слова inherited наследуемый конструктор родительского класса. А затем задаются начальные значения различных полей. Кроме задания максимального возраста 150 лет, тут исправляются еще некоторые недостатки нашего класса. Задается значение пола равное нулевому символу. По такому значению в дальнейшем можно проверять, был ли указан пол личности. И задаются строки «неизвестный» в качестве начального значения строковых нолей. Так что теперь, если какие-то данные о личности неизвестны, вместо них будет выдаваться этот текст, а не пустая строка. Думаю, что пользователю это будет удобно.

Теперь рассмотрим деструкторы. Это специальные методы, уничтожающие объект и освобождающие занимаемую им память. Деструктор автоматически вызывается при выполнении метода Free объекта класса. Если в вашем классе деструктор не объявлен, вызывается деструктор родительского класса.

В большинстве случаев объявлять деструктор в классе не требуется. И мы до сих пор прекрасно без него обходились. Деструктор нужен в тех случаях, когда в конструкторе или в каком-то методе класса создается объект, динамически размещаемый в памяти. Тогда нужен деструктор, чтобы уничтожить этот объект и освободить занимаемую им память. Аналогично деструктор требуется, если объект создает и хранит информацию в каких-то временных файлах. Тогда в деструкторе надо уничтожить эти файлы, чтобы они не оставались на диске.

Давайте введем в наш класс TPerson еще одно добавление, которое расширит его возможности. Создадим возможность хранить форматированный текст в формате .rtf, который может поступать, как вы знаете, из окна RichEdit. Но форматированный текст можно хранить или в объекте класса TRichEdit, или в файле, в который он записан методом SaveToFile свойства Lines окна RichEdit. Создать в нашем классе внутренний объект класса TRichEdit мы не можем. Так что остается вариант хранения во временном файле.
Добавьте в класс TPerson два поля:

FDoc: ^TRichEdit;
FileTMP: string;

Поле FDoc будет служить указателем на внешний компонент класса TRichEdit, из которого будет загружаться форматированный текст в файл, и в который будет грузиться текст из файла. А в переменной FileTmp будем хранить имя временного файла. Чтобы компилятор принял объявление поля FDoc, он должен понять идентификатор TRichEdit. Этот класс объявлен в модуле ComCtrls. Так что добавьте ссылку на этот модуль в предложение uses.
Введите в открытый раздел класса объявления двух процедур:

procedure SetDoc(var Value: TRichEdit); procedure GetDoc(var Value: TRichEdit);

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

В процедуре SetDoc сначала проверяется, задавалось ли уже имя временного файла. Если нет, то это имя формируется из строки FName (фамилия, имя, отчество) и расширения .tmp. Затем переменной FDoc задается адрес компонента TRichEdit. А дальнейшее понятно: форматированный текст записывается в файл с именем, хранящимся в переменной FileTMP.

Добавьте в свое тестовое приложение окно RichEdit. Можете добавить также компонент FontDialog и обеспечить возможность форматирования текста в окне RichEdit. В обработчик щелчка на кнопке Запись вставьте оператор:

SetDoc(RichEditl);

А в обработчик щелчка на кнопке Чтение вставьте оператор:

GetDoc(RichEditl);

Выполнив тестовое приложение, можете убедиться, что ваш класс стал намного мощнее и может теперь хранить форматированный текст. Но он пока не совсем правильно оформлен. Если в приложении выполнялся вызов процедуры SetDoc, то был создан временный файл. И когда приложение завершается, этот файл остается на диске. Вам нужно написать деструктор, который удалял бы этот файл.

Объявление деструктора выглядит так же, как объявление процедуры, но предваряется ключевым словом destructor. В качестве имени деструктора обычно задают имя Destroy. Реализация деструктора, как правило, завершается вызовом наследуемого деструктора с помощью ключевого слова inherited, чтобы освободить память, отведенную для наследуемых полей.

В нашем случае в открытый раздел класса следует ввести объявление деструктора:

destructor Destroy; override;

Смысл ключевого слова override вы узнаете позднее. Пока просто напишите его, не задумываясь о его назначении. Реализация деструктора имеет вид:

Если файл создавался, то он уничтожается. Введение такого деструктора никак не отразится на внешнем поведении вашего тестового приложения. Но теперь вы не оставляете на диске временный и уже ненужный файл.

Следующий урок будет посвящён методам, наследованиям классов, операциям с классами.

Удачи!
Встретимся в следующем уроке!

Замечательные уроки для новичков 59(классы). 61 (свойства классов) воспроизводяться почти до конца. Но почти. Было бы очень желательно (для новичков опять же) привести законченый лучше зазипований проект в DElphi 7 на уровне информации 61 урока, что бы можно было сравнить работающий проект автора урока с проектом и процедурами собраными новичком на основе информации уроков 59-61 и найти ошибку. Заранее благодарен.

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

Шаг 1. Придумывание идеи

Первым шагом нужно ответить себе на вопрос: "Для чего мне этот компонент и что он будет делать?". Затем необходимо в общих чертах продумать его свойства, события, на которые он будет реагировать и те функции и процедуры, которыми компонент должен обладать. Затем очень важно выбрать "предка" компонента, то есть наследником какого класса он будет являться. Тут есть два пути. Либо в качестве наследника взять уже готовый компонент (то есть модифицировать уже существующий класс), либо создать новый класс.

  1. Создание Windows-элемента управления (TWinControl)
  2. Создание графического элемента управления (TGraphicControl)
  3. Создание нового класса или элемента управления (TCustomControl)
  4. Создание невизуального компонента (не видимого) (TComponent)

Невизуальные компоненты видны только во время разработки приложения (Design-Time), а во время работы приложения (Run-Time) их не видно, но они могут выполнять какую-нибудь работу. Наиболее часто используемый невизуальный компонент - это Timer.

Итак, что бы приступить от слов к делу, попробуем сделать какой-нибудь супер простой компонент (только в целях ознакомления с техникой создания компонентов), а потом будем его усложнять.

Шаг 2. Создание пустого модуля компонента

Рассматривать этот шаг я буду исходя из устройства Дельфи 3, в других версиях этот процесс не сильно отличается. Давайте попробуем создать кнопку, у которой будет доступна информация о количестве кликов по ней.

  • Закройте проекты, которые вы разрабатывали (формы и модули)
  • В основном меню выберите Component -> New Component.
  • Перед вами откроется диалоговое окно с названием "New Component"
  • В поле Ancestor Type (тип предка) выберите класс компонента, который вы хотите модифицировать. В нашем случае вам надо выбрать класс TButton
  • В поле Class Name введите имя класса, который вы хотите получить. Имя обязательно должно начинаться с буквы "T". Мы напишем туда, например, TCountBtn
  • В поле Palette Page укажите имя закладки на которой этот компонент появиться после установки. Введем туда MyComponents (теперь у вас в Делфьи будет своя закладка с компонентами!).
  • Поле Unit File Name заполняется автоматически, в зависимости от выбранного имени компонента. Это путь куда будет сохранен ваш модуль.
  • В поле Search Path ничего изменять не нужно.
  • Теперь нажмите на кнопку Create Unit и получите следующее:

Шаг 3. Начинаем разбираться во всех директивах

Что же здесь написано? да собственно пока ничего интересного. Здесь объявлен новый класс TCountBtn и процедура регистрации вашего компонента в палитре компонентов.

Директива Private. Здесь вы будете писать все скрытые поля которые вам понадобятся для создания компонента. Так же в этой директиве описываются процедуры и функции, необходимые для работы своего компонента, эти процедуры и функции пользователю не доступны. Для нашего компонент мы напишем туда следующее (запись должна состоять из буквы "F" имени поля: тип этого поля): Буква "F" должна присутсвовать обязательно. Здесь мы создали скрытое поле Count, в котором и будет храниться число кликов по кнопке.

Директива Protected. Обычно я здесь пишу различные обработчики событий мыши и клавиатуры. Мы напишем здесь следующую строку:

Это указывает на то, что мы будем обрабатывать щелчок мыши по компоненту. Слово "override" указывает на то, что мы перекроем стандартное событие OnClick для компонента предка.

Шаг 4. Пишем процедуры и функции.

Начнем с написания конструктора. Это делается примерно так: Здесь в принципе понимать ничего не надо. Во всех своих компонентах я писал именно это (только класс компонента менял и все). Также сюда можно записывать любые действия, которые вы хотите сделать в самом начале работы компонента, то есть в момент установки компонента на форму. Например можно установить начальное значение нашего свойства Count. Но мы этого делать не будем.

Теперь мы напишем процедуру обработки щелчка мышкой по кнопке: "Inherited click" означает, что мы повторяем стандартные методы обработки щелчка мышью (зачем напрягаться и делать лишнюю работу:)).

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

Шаг 5. Устанавливаем компонент

Если вы сумели написать и понять, все то что здесь предложено, то установка компонента не должна вызвать у вас никаких проблем. Все здесь делается очень просто. В главном меню выберите пункт Component -> Install Component. перед вами открылось диалоговое окно Install Component. В нем вы увидите две закладки: Into exsisting Package и Into new Package. Вам предоставляется выбор установить ваш компонент в уже существующий пакет или в новый пакет соответственно. Мы выберем в уже существующий пакет.

В поле Unit File Name напишите имя вашего сохранненого модуля (естественно необходимо еще и указать путь к нему), а лучше воспользуйтесь кнопкой Browse и выберите ваш файл в открывшемся окне.

В Search Path ничего изменять не нужно, Делфьи сама за вас все туда добавит.

В поле Package File Name выберите имя пакета, в который будет установлен ваш компонент. Мы согласимся с предложенным по умолчанию пакетом.

Создание свойств своего типа

Теперь мы попробуем создать свойство нестандартного типа. Рассмотрим это на примере метки - TLabel. У этого компонента есть такое свойство: Alignment. Оно может принимать следующие значения: taLeftJustify, taCenter, taRightJustify. Приступаем к созданию свойства. Ничего интересного мне придумать не удалось, но тем не менее я вам покажу это на примере того свойства, которое я придумал. Оно очень простое и поможет вам разобраться. Свойство будет называться ShowType (тип TShowTp), в нашем компоненте оно будет отвечать за отображение свойства Count. Если пользователь установит свойство ShowType в Normal, то кнопка будет работать, как и работала. А если пользователь присвоит этому свойтсву значение CountToCaption, то количество кликов, будет отображаться на самой кнопке.

Для начале нам необходимо объявить новый тип. Описание типа нужно добавить после слова Type. Вот так это выглядело вначале: Вот так это должно выглядеть: Здесь мы объявили новый тип TShowTp, который может принимать только два значения. Все значения, которые вы хотите добавить перечисляются через запятую.

Теперь нам понадобиться создать поле этого типа. Это мы уже умеем и делать и поэтому не должно вызвать никаких сложностей. В директиву Private напишите: Мы создали поле ShowType, типа TShowTp.

Конечно же необходимо добавить это свойство в инспектор объектов: Ну и наконец, чтобы наш компонент реагировал на изменение этого свойства пользователем надо слегка изменить обработчик события OnClick. После небольшой модификации он может иметь примерно такой вид: Объясню что произошло. Вначале мы увеличиваем счетчик на единицу. Затем проверяем какое значение имеет свойство ShowType. Если Normal, то ничего не делаем, а если CountToCaption, то в надпись на кнопке выводим количество кликов. Не так уж и сложно как это могло показаться с первого раза.

Имплантируем таймер в компонент

Очень часто бывает, что вам необходимо вставить в компонент, какой-нибудь другой компонент, например, таймер. Как обычно будем рассматривать этот процесс на конкретном примере. Сделаем так, что через каждые 10 секунд значение счетчика кликов будет удваиваться. Для этого мы встроим таймер в нашу кнопку. Нам понадобиться сделать несколько несложных шагов.

После раздела uses, где описаны добавленные в программу модули, объявите переменную типа TTimer. Назовем ее Timer. Приведу небольшой участок кода: Дальше в директиву Protected необходимо добавить обработчик события OnTimer для нашего таймера. Это делается так: Поскольку наш таймер это не переменная, а компонент, его тоже надо создать, для этого в конструктор нашей кнопки напишем: Здесь создается экземпляр нашего таймера и его свойству Iterval (измеряется в миллисекундах) присваивается значение 10000 (то есть 10 секунд если по простому).
Собственно осталось написать саму процедуру OnTimer. Я сделал это так:

Вот примерно то, что у вас должно получиться в конце: Если у вас что-то не сработало, то в начале проверьте все ли у вас написано правильно. Затем проверьте может у вас не хватает какого-нибудь модуля в разделе Uses.

Переустановка компонента

Очень часто бывает необходимо переустановить ваш компонент. Если вы попробуете сделать это путем выбора Component->Install Component, то Дельфи вас честно предупредит о том, что пакет уже содержит модуль с таким именем. Перед вами открывается окно с содержимым пакета. В нем вы должны найти имя вашего компонента и удалить его (либо нажать кнопочку Remove). Теперь в пакете уже нет вашего компонента. Затем проделайте стандартную процедуру по установке компонента.

Когда читаешь о той или иной реализации механизмов ООП, то весьма забавно видеть, как та или иная конкретная особенность конкретного языка или библиотеки называется «очевидной». Особенно выделяется в этом отношении описание ООП в C++, в котором по факту реализация одна из самых непрозрачных и запутанных.
Так вот, я ничего не буду писать про очевидность, а расскажу про способ облегчить себе жизнь в критических точках жизненного цикла объектов, используя не «очевидные», но весьма полезные особенности реализации ООП в Delphi.

Обработка невозможности завершить создание объекта в конструкторе

Если конструктор хранит свои данные только в полях объекта, то решение элементарно — достаточно возбудить исключение. RTL Delphi сама его перехватит, вызовет деструктор, высвободит занятую память и возбудит исключение повторно.
Соответственно, если часть данных для конструирования хранится в глобальных переменных, то достаточно использовать обычный блок try..except с повторным возбуждением исключения.
Отсюда выводятся два требования к деструкторам: не провоцировать исключения, что означает не пытаться заниматься чем-либо, кроме освобождения ресурсов (например, сохранением настроек) и обязательно поддерживать…

Удаление частично инициализированного объекта

В Delphi это не представляет никаких трудностей, так как любой объект еще до передачи управления конструктору инициализируется нулями. Соответственно, в деструкторе достаточно корректно обрабатывать нулевые значения, в чем сильно помогает процедура FreeAndNil, которая освобождает объект, только если ссылка на него уже не nil.

Порядок вызова конструкторов

В Delphi он не имеет значения. Можно вызывать конструкторы предка, другие конструкторы того же класса точно так же, как и обычные методы или не вызывать ничего — так что если надо использовать инициализацию предка, то забывать ее вызвать не рекомендуется, зато и проблем сделать что-то до или вместо вызова конструктора предка нет никаких.

Вызов виртуальных методов в конструкторе

Так как в Delphi объект сразу инициализируется конечной ссылкой на VMT, то никакой разницы между вызовами виртуальных методов из конструктора от прочих вариантов нет.

Виртуальные конструкторы и ссылки на класс

В Delphi конструкторы могут быть виртуальными. Смысл такой особенности — возможность создания объектов с неизвестным на этапе компиляции классом без необходимости реализации фабрики. С этой целью используется переменные со ссылкой на класс (а не объект!), для которых и можно вызвать виртуальный конструктор, получив экземпляр соответствующего класса или его потомка, в зависимости от значения ссылки.

Автоматическое управление временем жизни объекта

Если объект реализует тот или иной явно заданный интерфейс, то его можно привести к ссылке на этот интерфейс, используя присваивание или операцию as. В этом случае Delphi будет сама генерировать вызовы методов IUnknown, что позволяет не запрашивать интерфейсы и не удалять объекты явно.

Итоги


С одной стороны, все вышеперечисленное должен знать каждый разработчик; с другой — я встречал не так уж мало программистов со стажем, изобретающих велосипеды только от незнания особенностей реализации своего рабочего инструмента. Надеюсь, что эта статья немного поможет. Из полезных особенностей реализации ООП в Delphi осталась нерассмотренной поддержка делегирования реализации интерфейсов, но это тема для отдельной статьи.


Когда я учился писать многопоточные приложения — я перечитал кучу литературы и справочной информации по этой области. Но между теорией и практикой — огромная пропасть. Я набил кучу шишек, и до сих пор иногда получаю по голове от собственных потоков. Для себя я выработал набор некоторых правил, которым стараюсь строго следовать, и это значительно помогает мне в написании многопоточного кода.

В данной статье я поделюсь своим стилем написания кода на нижнем уровне абстракции. Поскольку я дельфист, то все примеры будут на Delphi, однако все нижесказанное справедливо и для других языков программирования (позволяющих работать с объектами синхронизации конечно)

Потокобезопасный объект

Первое правило — между потоками работать только с потокобезопасными объектами. Это самое простое, логичное и понятное правило. Однако даже тут есть некоторые особенности. Объект должен быть целиком потокобезопасный, а это значит что все public методы (кроме конструктора и деструктора) нужно синхронизировать. Конструкторы и деструкторы в свою очередь должны быть всегда синхронизированы снаружи объекта. Одна из ошибок на ранних этапах работы с потоками — я забывал о синхронизации конструкторов и деструкторов. И если в делфи с конструктором проблем нет (мы получаем указатель на объект только когда конструктор уже отработал), то с деструктором надо быть внимательным. Синхронизация деструкторов — очень скользкая тема, и я не могу дать каких-либо указаний, как лучше её реализовывать (я не гений многопоточного программирования, а только учусь ;) ). Сам я стараюсь проводить такую синхронизацию через деструктор класса TThread, но это справедливо только для объектов, которые существуют всю жизнь потока.

Блокировки

Описание
Примеры

Понимание и строгое выполнение данного условия — ключ к отсутствию deadlock-ов. Давайте рассмотрим на примере, о чем я говорю. Допустим у нас есть некоторый класс:

Следуя тому, что у нас должен быть потокобезопасный объект — я реализовал свойства A и B через геттеры и сеттеры с критической секцией:

Допустим функция DoSomething у нас работает с A и B как-то так:

Эй, но мы ведь используем одну критическую секцию для A и для B, скажет неискушенный писатель. И сразу же «оптимизирует» этот кусок:

И это будет ошибка. Теперь, если мы в обработчике WM_MYMESSAGE попытаемся обратиться к полю A или B — мы получим дедлок. Данный дедлок — очевиден, так как объем кода маленький, данные простые. Но оно становится не тривиальным, когда когда код огромен, появляется куча связей и зависимостей. Согласно правилу — работать только с одной синхронизацией одновременно, вышеописанный код можно «оптимизировать» так:

Поэтому всегда, прежде чем вызвать новую синхронизацию — нужно освободить другие объекты синхронизации. Код в духе:

В большинстве случаев можно считать многопоточным быдлокодом. Я думаю вы уже представляете как надо его переписать:

По данному подходу видно, что нам приходится копировать данные, что может отразиться на производительности. Однако в большинстве случаев объемы данных не велики, и мы можем позволить копировать их. Трижды четырежды подумайте, чтобы применять подход без копирования.

Диагностика

На уровне компилирования такое диагностировать не получится. Однако можно провести диагностику в реалтайме. Для этого нам надо хранить текущий объект синхронизации для каждого потока. Вот пример реализации средства диагностики на Delphi.

Вызываем InitSyncObject когда стартуем новый поток.
Перед захватом объекта синхронизации вызываем PushThreadObject, после освобождения объекта синхронизации вызываем PopThreadObject.
Для удобства использования данных функций рекомендую скопировать код модуля SyncObjs.pas в новый, скажем SyncObjsDbg.pas. В нем есть базовый класс объекта синхронизации:

В Acquire добавить вызов PushSyncObject(Self), а в Release PopSyncObject. Так же не забываем обрамить в эти функции WaitFor методы у THandleObject. Кроме того, если используем метод TThread.Synchronize то до вызова сохраняем объект TThread, а после извлекаем его (PopSyncObject), если используем API функции SendMessage или WaitFor функции, то до вызова сохраняем хендл (PushSyncObject), после — извлекаем (PopSyncObject).
Вот и все, теперь при попытке захватить второй объект синхронизации — будет возникать исключение, а модули (SyncObjs/SyncObjsDbg) можно менять через дефайны.

Плохой код

В качестве примера плохого кода возьмем… класс TThreadList из модуля Classes.pas

Казалось бы, потокобезопасный класс, с доступом через критическую секцию, что в нем плохого? А плохо то, что у нас доступны методы LockList и UnlockList. Если между парой вызовов LockList и UnlockList у нас будет синхронизация — то мы нарушаем вышеописанное правило. Поэтому выносить пару функций Lock/Unlock в паблик — не есть хорошо, и такие функции нужно использовать крайне осторожно.

К слову, различные API от Microsoft часто возвращают Enum интерфейсы, вот например. Зачем они это делают? Ведь гораздо удобнее получить количество скажем через функцию Count, а потом в цикле через функцию GetItem по индексу получать элемент. Но в этом случае им бы пришлось вынести еще пару функций Lock/Unlock, чтобы никто не мог изменить список, пока вы в цикле работаете. Кроме того, если вы между Lock/Unlock вдруг вызовете такую API функцию, которая выполняет внутреннюю синхронизацию — вы запросто можете получить дедлок. Поэтому все и сделано через Enum интерфейсы. При получении такого интерфейса формируется список объектов, и счетчик ссылок их увеличивается. Это значит что ни один объект в Enum интерфейсе не будет уничтожен пока как минимум энум интерфейс существует, и пока вы работаете с Enum — ко внутреннему списку все имеют доступ, и этот список может даже изменяться.

Наверное хватит


Нажал я кнопку предпросмотр, увидел получившийся объем, и понял что пока хватит. В следующей статье я хотел бы рассказать про класс делфи TThread, и показать правила, которым я следую при создании и работе с потоками.

Читайте также: