Dynamic — Директива Delphi


Содержание

Программирование на языке Delphi

Глава 5. Динамически загружаемые библиотеки


Авторы: А.Н. Вальвачев
К.А. Сурков
Д.А. Сурков
Ю.М. Четырько

Опубликовано: 03.12.2005
Исправлено: 10.12.2020
Версия текста: 1.0

До сих пор создаваемые нами программы были монолитными и фактически состояли из одного выполняемого файла. Это, конечно, очень удобно, но не всегда эффективно. Если вы создаете не одну программу, а несколько, и в каждой из них пользуетесь общим набором подпрограмм, то код этих подпрограмм включается в каждую вашу программу. В результате достаточно большие общие части кода начинают дублироваться во всех ваших программах, неоправданно «раздувая» их размеры. Поддержка программ затрудняется, ведь если вы исправили ошибку в некоторой подпрограмме, то вам придется перекомпилировать и переслать потребителю целиком все программы, которые ее используют. Решение проблемы напрашивается само собой — перейти к модульной организации выполняемых файлов. В среде Delphi эта идея реализуется с помощью динамически загружаемых библиотек. Техника работы с ними рассмотрена в данной главе.

5.1. Динамически загружаемые библиотеки

Динамически загружаемая библиотека (от англ. dynamically loadable library) — это библиотека подпрограмм, которая загружается в оперативную память и подключается к использующей программе во время ее работы (а не во время компиляции и сборки). Файлы динамически загружаемых библиотек в среде Windows обычно имеют расширение .dll (от англ. Dynamic-Link Library). Для краткости в этой главе мы будем использовать термин динамическая библиотека, или даже просто библиотека, подразумевая DLL-библиотеку.

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

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

5.2. Разработка библиотеки


5.2.1. Структура библиотеки

По структуре исходный текст библиотеки похож на исходный текст программы, за исключением того, что текст библиотеки начинается с ключевого слова library , а не слова program . Например:

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

Если в теле библиотеки объявлены некоторые процедуры,

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

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

Ниже приведен пример исходного текста простейшей динамически загружаемой библиотеки SortLib. Она содержит единственную процедуру BubleSort, сортирующую массив целых чисел методом «пузырька»:

Исходный текст динамически загружаемой библиотеки заканчивается операторным блоком begin. end , в который можно вставить любые операторы для подготовки библиотеки к работе. Эти операторы выполняются во время загрузки библиотеки основной программой. Наша простейшая библиотека SortLib не требует никакой подготовки к работе, поэтому ее операторный блок пустой.

5.2.2. Экспорт подпрограмм

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

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

то это означает, что экспортное имя процедуры будет ’BubleSort’. При желании это имя можно сделать отличным от программного имени, дополнив описание директивой name , например:

В итоге, экспортное имя процедуры BubleSort будет ’BubleSortIntegers’.

Экспортные имена подпрограмм должны быть уникальны в пределах библиотеки, поэтому их нужно всегда указывать явно для перегруженных ( overload ) процедур и функций. Например, если имеются две перегруженные процедуры с общим именем QuickSort,

то при экспорте этим двум процедурам необходимо явно указать отличные друг от друга экспортные имена:

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

5.2.3. Соглашения о вызове подпрограмм

В главе 2 мы уже кратко рассказывали о том, что в различных языках программирования используются различные правила вызова подпрограмм, и что для совместимости с ними в языке Delphi существуют директивы register , stdcall , pascal и cdecl . Применение этих директив становится особенно актуальным при разработке динамически загружаемых библиотек, которые используются в программах, написанных на других языках программирования.

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

Стек — это область памяти, в которую данные помещаются в прямом порядке, а и извлекаются в обратном, по аналогии с наполнением и опустошением магазина патронов у стрелкового оружия. Очередность работы с элементами в стеке обозначается термином LIFO (от англ. Last In, First Out — последним вошел, первым вышел).

Существует еще обычная очередность работы с элементами, обозначаемая термином FIFO (от англ. First In, First Out — первым вошел, первым вышел).

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

Вызов подпрограммы состоит из «заталкивания» в стек всех аргументов и адреса следующей команды (для воврата к ней), а затем передачи управления на начало подпрограммы. По окончании работы подпрограммы из стека извлекается адрес воврата с передачей управления на этот адрес; одновременно с этим из стека выталкиваются аргументы. Происходит так называемая очистка стека. Это общая схема работы и у нее бывают разные реализации. В частности, аргументы могут помещаться в стек либо в прямом порядке (слева направо, как они перечислены в описании подпрограммы), либо в обратном порядке (справа налево), либо вообще, не через стек, а через свободные регистры процессора для повышения скорости работы. Кроме того, очистку стека может выполнять либо вызываемая подпрограмма, либо вызывающая программа. Выбор конкретного соглашения о вызове обеспечивают директивы register , pascal , cdecl и stdcall . Их смысл поясняет таблица 5.1.

Директива Порядок занесения аргументов в стек Кто отвечает за очистку стека Передача аргументов через регистры
register Слева направо Подпрограмма Да
pascal Слева направо Подпрограмма Нет
cdecl Справа налево Вызывающая программа Нет
stdcall Справа налево Подпрограмма Нет
Таблица 5.1. Соглашения о вызове подпрограмм
ПРИМЕЧАНИЕ

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

Возникает резонный вопрос: какое соглашение о вызове следует выбирать для процедур и функций динамически загружаемых библиотек. Ответ — соглашение stdcall :

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

5.2.4. Пример библиотеки

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

Шаг 1. Запустите систему Delphi и выберите в меню команду File | New | Other. . В диалоговом окне, которое откроется на экране, выберите значок с подписью DLL Wizard и нажмите кнопку OK (рисунок 5.1):

Рисунок 5.1. Окно выбора нового проекта, в котором выделен пункт DLL Wizard

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

Шаг 2. С помощью команды File | New | Unit создайте в проекте новый программный модуль. Его заготовка будет выглядеть следующим образом:

Шаг 3. Сохраните модуль под именем SortUtils.pas, а проект — под именем SortLib.dpr. Прейдите к главному файлу проекта и удалите из секции uses модули SysUtils и Classes (они сейчас не нужны). Главный программный модуль должен стать следующим:

Шаг 4. Наберите исходный текст модуля SortUtils:

В этом модуле процедуры BubleSort и QuickSort сортируют массив чисел двумя способами: методом «пузырька» и методом «быстрой» сортировки соответственно. С их реализацией мы предоставляем вам разобраться самостоятельно, а нас сейчас интересует правильное оформление процедур для их экспорта из библиотеки.

Директива stdcall , использованная при объявлении процедур BubleSort и QuickSort,

позволяет вызывать процедуры не только из программ на языке Delphi, но и из программ на языках C/C++ (далее мы покажем, как это сделать).

Благодаря присутствию в модуле секции exports ,

подключение модуля в главном файле библиотеки автоматически приводит к экспорту процедур.

Шаг 5. Сохраните все файлы проекта и выполните компиляцию. В результате вы получите на диске в своем рабочем каталоге двоичный файл библиотеки SortLib.dll. Соответствующее расширение назначается файлу автоматически, но если вы желаете, чтобы компилятор назначал другое расширение, воспользуйтесь командой меню Project | Options… и в появившемся окне Project Options на вкладке Application впишите расширение файла в поле Target file extension (рисунок 5.2).

Рисунок 5.2. Окно настройки параметров проекта

Кстати, с помощью полей LIB Prefix , LIB Suffix и LIB Version этого окна вы можете задать правило формирования имени файла, который получается при сборке библиотеки. Имя файла составляется по формуле:

5.3. Использование библиотеки в программе

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

  • статический импорт (обеспечивается директивой компилятора external );
  • динамический импорт (обеспечивается функциями LoadLibrary и GetProcAddress ).

Статический импорт является более удобным, а динамический — более гибким.

5.3.1. Статический импорт

При статическом импорте все действия по загрузке и подключению библиотеки выполняются автоматически операционной системой во время запуска главной программы. Чтобы задействовать статический импорт, достаточно просто объявить в программе процедуры и функции библиотеки как внешние. Это делается с помощью директивы external , например:

После ключевого слова external записывается имя двоичного файла библиотеки в виде константной строки или константного строкового выражения. Вместе с директивой external может использоваться уже известная вам директива name , которая служит для явного указания экспортного имени процедуры в библиотеке. С ее помощью объявления процедур можно переписать по-другому:

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

Шаг 6. Создайте новую консольную программу. Для этого выберите в меню команду File | New | Other. и в открывшемся диалоговом окне выделите значок Console Application . Затем нажмите кнопку OK .

Шаг 7. Добавьте в программу external -объявления процедур BubleSort и QuickSort, а также наберите приведенный ниже текст программы. Сохраните проект под именем TestStaticImport.dpr.

Шаг 8. Выполните компиляцию и запустите программу. Если числа печатаются на экране по возрастанию, то сортировка работает правильно.

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

5.3.2. Модуль импорта

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

Модуль импорта для библиотеки SortLib будет выглядеть так:

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

5.3.3. Динамический импорт

Действия по загрузке и подключению библиотеки (выполняемые при статическом импорте автоматически) можно проделать самостоятельно, обратившись к стандартным функциям операционной системы. Таким образом, импорт можно произвести динамически во время работы программы (а не во время ее запуска).

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

Ниже приведено краткое описание функций LoadLibrary , FreeLibrary и GetProcAddress .

  • LoadLibrary (LibFileName: PChar): HModule — загружает в оперативную память библиотеку, которая хранится на диске в файле с именем LibFileName . При успешном выполнении функция возвращает числовой описатель библиотеки, который должен использоваться в дальнейшем для управления библиотекой. Если при загрузке библиотеки призошла какая-нибудь ошибка, то возвращается нулевое значение. Если аргумент LibFileName содержит имя файла без маршрута, то этот файл ищется в следущих каталогах: в каталоге, из которого была запущена главная программа, в текущем каталоге, в системном каталоге операционной системы Windows (его точный маршрут можно узнать вызовом функции GetSystemDirectory ), в каталоге, по которому установлена операционная система (его точный маршрут можно узнать вызовом функции GetWindowsDirectory ), а также в каталогах, перечисленных в переменной окружения PATH .
  • FreeLibrary (LibModule: HModule): Bool — выгружает библиотеку, заданную описателем LibModule , из оперативной памяти и освобождает занимаемые библиотекой ресурсы системы.
  • GetProcAddress (Module: HModule; ProcName: PChar): Pointer — возвращает адрес подпрограммы с именем ProcName в библиотеке с описателем Module . Если подпрограмма с именем ProcName в библиотеке не существует, то функция возвращает значение nil (пустой указатель).

Приведенная ниже программа TestDynamicImport аналогична по функциональности программе TestStaticImport , но вместо статического импорта использует технику динамического импорта:

В программе определены два процедурных типа данных, которые по списку параметров и правилу вызова ( stdcall ) соответствуют подпрограммам сортировки BubleSort и QuickSort в библиотеке:

Эти типы данных нужны для объявления процедурных переменных, в которых сохраняются адреса подпрограмм:

В секции var объявлена также переменная для хранения целочисленного описателя библиотеки, возвращаемого функцией LoadLibrary :

Программа начинает свою работу с того, что вызывает функцию LoadLibrary , в которую передает имя файла DLL-библиотеки. Функция возвращает описатель библиотеки, который сохраняется в переменной LibHandle .

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

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

Если этот адрес отличен от значения nil , значит подпрограмма с указанным именем была найдена в библиотеке и ее можно вызвать путем обращения к процедурной переменной:

По окончании сортировки программа выгружает библиотеку вызовом функции FreeLibrary .

Как вы убедились, динамический импорт в сравнении со статическим требует значительно больше усилий на программирование, но он имеет ряд преимуществ:

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

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

5.4. Использование библиотеки из программы на языке C++

Созданные в среде Delphi библиотеки можно использовать в других языках программирования, например в языке C++. Язык C++ получил широкое распространение как язык системного программирования, и в ряде случаев программистам приходится прибегать к нему.

Ниже показано, как выполнить импорт подпрограмм BubleSort и QuickSort в языке C++.

Не углубляясь в детали синтаксиса, заметим, что в языке C++ отсутствуют открытые массивы в параметрах подпрограмм. Тем не менее, программист может вызывать такие подпрограммы, основываясь на том, что открытый массив неявно состоит из двух параметров: указателя на начало массива и номера последнего элемента.

5.5. Глобальные переменные и константы

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

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

5.6. Инициализация и завершение работы библиотеки

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

Существует еще один способ инициализации и завершения библиотеки, основанный на использовании предопределенной переменной DllProc . Переменная DllProc хранит адрес процедуры, которая автоматически вызывается при отключении библиотеки от программы, а также при создании и уничтожении параллельных потоков в программах, использующих DLL-библиотеку (потоки обсуждаются в главе 14). Ниже приведен пример использования переменной DllProc :

Процедура LibExit получает один целочисленный аргумент, который уточняет причину вызова. Возможные значения аргумента:

  • DLL_PROCESS_DETACH — отключение программы;
  • DLL_PROCESS_ATTACH — подключение программы;
  • DLL_THREAD_ATTACH — создание параллельного потока;
  • DLL_THREAD_DETACH — завершение параллельного потока.

Обратите внимание, что установка значения переменной DllProc выполняется в главном программном блоке, причем предыдущее значение сохраняется для вызова «по цепочке».

Мы рекомендуем вам прибегать к переменной DllProc лишь в том случае, если библиотека должна реагировать на создание и уничтожение параллельных потоков. Во всех остальных случаях лучше выполнять инициализацию и завершение с помощью секций initialization и finalization .

5.7. Исключительные ситуации и ошибки выполнения подпрограмм

Для поддержки исключительных ситуаций среда Delphi использует средства операционной системы Window. Поэтому, если в библиотеке возникает исключительная ситуация, которая никак не обрабатывается, то она передается вызывающей программе. Программа может обработать эту исключительную ситуацию самым обычным способом — с помощью операторов try … except . end . Такие правила действуют для программ и DLL-библиотек, созданных в среде Delphi. Если же программа написана на другом языке программирования, то она должна обрабатывать исключение в библиотеке, написанной на языке Delphi как исключение операционной системы с кодом $0EEDFACE. Адрес инструкции, вызвавшей исключение, содержится в первом элементе, а объект, описывающий исключение, — во втором элементе массива ExceptionInformation , который является частью системной записи об исключительной ситуации.

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

5.8. Общий менеджер памяти

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

Модуль ShareMem является модулем импорта динамически загружаемой библиотеки Borlndmm.dll, которая должна распространяться вместе с вашей программой. В момент инициализации модуль ShareMem выполняет подмену стандартного менеджера памяти на менеджер памяти из библиотеки Borlndmm.dll. Благодаря этому библиотека и программа могут выделять и освобождать память совместно.

Модуль ShareMem следует подключать еще и в том случае, если между библиотекой и программой происходит передача длинных строк или динамических массивов. Поскольку длинные строки и динамические массивы размещаются в динамической памяти и управляются автоматически (путем подсчета количества ссылок), то блоки памяти для них, выделяемые программой, могут освобождаться библиотекой (а также наоборот). Использование единого менеджера памяти из библиотеки Borlndmm.dll избавляет программу и библиотеку от скрытых разрушений памяти.

ПРИМЕЧАНИЕ

Последнее правило не относится к отрытым массивам-параметрам, которые мы использовали в подпрограммах BubleSort и QuickSort при создании библиотеки SortLib.dll.

5.9. Стандартные системные переменные

Как вы уже знаете, в языке Delphi существует стандартный модуль System , неявно подключаемый к каждой программе или библиотеке. В этом модуле содержатся предопределенные системные подпрограммы и переменные. Среди них имеется переменная IsLibrary с типом Boolean, значение которой равно True для библиотеки и False для обычной программы. Проверив значение переменной IsLibrary , подпрограмма может определить, является ли она частью библиотеки.

В модуле System объявлена также переменная CmdLine: PChar , содержащая командную строку, которой была запущена программа. Библиотеки не могут запускаться самостоятельно, поэтому для них переменная CmdLine всегда содержит значение nil .

5.10. Итоги

Прочитав главу, вы наверняка вздохнули с облегчением. Жизнь стала легче: сделал одну уникальную по возможностям библиотеку и вставляй ее во все программы! Нужно подключить к Delphi-программе модуль из другой среды программирования — пожалуйста! И все это делается с помощью динамически загружаемых библиотек. Надеемся, вы освоили технику работы с ними и осилите подключение к своей программме библиотек, написанных не только на языке Delphi, но и на языках C и C++. В следующей главе мы рассмотрим некоторые другие взаимоотношения между программами, включая управление объектами одной программы из другой.

Динамическое создание компонентов в Delphi

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

В результате на форме при нажатии кнопки появится новый компонент TLabel/

Созданные подобным образом компоненты могут быть изменены только программно так как их изначально нет на форме (фрейме, модуле данных). Они создаются в процессе работы программы и поэтому они недоступны для визуального редактора. Именно по этой причине необходимо прямое указание параметров внешнего вида.

Назначать обработчики событий для таких компонентов также необходимо непосредственно в коде программы. В качестве примера зададим обработчик события OnClick для компонента TLabel созданного ранее.

Здесь LabelClick имя процедуры обработчика события.

В отношении любых динамических ресурсов необходимо следовать простому правилу – «ресурс взял – ресурс отдал. Чем скорее, тем лучше». Динамически создаваемые компоненты не исключение. Их необходимо уничтожать сразу же, как только отпадает необходимость в их использовании.

Сделать это можно одним из двух равнозначных способов. Первый состоит в том, чтобы вызвать метод Free компонента и затем присвоить соответствующей переменной значение nil.

Второй способ состоит в использовании процедуры FreeAndNil, которая выполнит оба указанных действия.

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

Работа с динамически создаваемыми компонентами имеет одну тонкость, связанную с особенностями работы редактора кода Delphi.

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

Чтобы этого избежать рекомендуется объявлять динамически созданные компоненты в других секциях. Например, private или public.

Неправильно:

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

2 мая 2013 г.

Эволюция Delphi: современные возможности

Среда Delphi не стоит на месте. Каждый год выходит новая версия Delphi с новыми возможностями. Компании Borland (Inprise) и Embarcadero всегда стремились сохранять в своих продуктах высокий уровень обратной совместимости, поэтому каждая новая версия Delphi способна почти без проблем компилировать старый код. Тем не менее, некоторые существующие возможности могут не существовать в новом мире и окружении или вести себя иначе.

В этой статье я попробую сделать небольшой обзор современных тенденций развития языка Delphi и изменений в нём. В целом, статья будет сконцентрирована на новейших измененияx в архитектуре Delphi, доступные в XE4.

Новый компилятор

Среда разработки состоит из компилятора (переводит исходный текст программы в машинный/виртуальный код), компоновщика/linker (собирает программу из готовых блоков, созданных компилятором), отладчика (debugger), редактора кода (и вообще, в целом — визуальной оболочки) и дополнительных утилит. Ну и, конечно же, среда разработки зависит от языка и библиотек на нём. Всё вместе это называется toolchain (букв. «цепочка утилит») — набор утилит для создания приложений. Слово «цепочка» намекает на то, что результат работы одной утилиты используется следующей (т.е. редактор -> компилятор -> компоновщик -> отладчик).

Среда Delphi является развитием языка Pascal. Toolchain Delphi является закрытой (проприетарной) разработкой Borland. За всю историю Delphi она поддерживала несколько платформ (Win16, Win32, Win64, Linux/CLX, .NET). Под каждую платформу был свой собственный компилятор, который был монолитным. Исходный код компилировался компилятором непосредственно в машинный код целевой платформы (файлы .dcu и .obj).

В этой ситуации добавление новой платформы было непростым делом, поскольку требовалось разрабатывать компилятор для неё с нуля. Дополнительными сложностями был перенос существующего код RTL и VCL, завязанного на конкретную платформу (Win32). Сегодня доля Windows уменьшается, а на сцену выходят более молодые платформы: от Apple и Google. Причём актуальные платформы меняются намного быстрее, чем это происходило в прошлом. В ситуации с таким динамическим изменением имеет смысл упростить разработку компилятора, чтобы более оперативно реагировать на изменения и вносить новые возможности.

Поэтому, центральной идеей ближайшего развития Delphi становится модульный компилятор. Идея заключается в том, чтобы разделить (ранее монолитный) компилятор на две части: т.н. front-end и back-end. Front-end компилятора берёт исходный код программы и переводит его не в машинный код конкретной платформы, а в (универсальный) виртуальный код — т.н. байт-код. Байт-код — это максимально универсальное представление логики программы, не зависящее от языка и платформы. Back-end работает по результату работы front-end: он преобразовывает байт-код уже непосредственно в машинный код конкретной платформы.

Таким образом, вместо того, чтобы делать компилятор полностью для каждой новой платформы, можно оставить front-end неизменным (а ведь именно он отвечает за синтаксис языка), а написать только новый back-end. Более того, вместо того, чтобы использовать собственную проприетарную (и ни с кем не совместимую) разработку, можно использовать широко известное решение (в качестве back-end, конечно же) — получив при этом не только частично готовый код, но и совместимость с некоторыми сторонними утилитами. В качестве такого известного решения разработчики Delphi решили использовать LLVM (Low Level Virtual Machine) — это универсальная система анализа, трансформации и оптимизации программ, реализующая виртуальную машину с RISC-подобными инструкциями.

LLVM используется, в частности, в компаниях Adobe, Apple и Google (например, iPhone SDK использует back-end LLVM). Apple и Google являются одними из основных спонсоров проекта. В настоящее время для LLVM есть back-end-ы для x86-32, x86-64, ARM, PowerPC, SPARC, MIPS, Qualcomm Hexagon и front-end-ы для С, C++, Objective-C, Fortran, Ada, Haskell, Java, Python, Ruby, JavaScript, GLSL (в т.ч. — Clang и GCC). А теперь ещё к front-end добавляется и Delphi. Конечно же, LLVM понятия не имеет про Паскаль и Borland-ский форматы файлов. Но Delphi может иметь свой собственный front-end, который будет компилировать исходный код Паскаль в байт-код LLVM (называемый LLVM IR — «Intermediate Representation», т.е. «промежуточное представление»). А готовый back-end от LLVM может скомпилировать IR от front-end Delphi в машинный код x86-32, x86-64 или ARM. Хотя LLVM IR похож на готовый байт-код для некой виртуальной машины или JIT-компилятора, он всё же нацелен именно на чёткое разграничение front-end и back-end и может рассматриваться как вывод компилятора — аналогично .dcu (Delphi) и .obj (C++ Builder) файлам.

Итак, теперь должно быть очевидным, что в будущем Delphi будет иметь новый компилятор, совместимый с LLVM — и начнётся это уже сейчас, начиная с компилятора для iOS (ARM). А для C++ Builder новая эра началась ещё в прошлом году: 64-битный компилятор C++ Builder сделан уже на новой архитектуре (LLVM). Конечно же, компилятор — это ещё не всё. Нужен ещё компоновщик, отладчик, библиотека поддержки языка (RTL), а для визуального языка — ещё и визуальная библиотека (такая как VCL, CLX, FMX). Также важно отметить, что LLVM в каком-то смысле «подталкивает» разработчиков front-end-ов использовать определённые подходы к управлению памятью, потоками и исключениями. Хотя это и всего лишь «толчок», а не железное ограничение. Стоит отметить, что для мобильных платформ распространена практика использовать LLVM (или виртуальные среды типа Java и .NET), которые поддерживают автоматическое управление памятью: или сборку мусора (garbage collection) или автоматические ссылки (ARC — Automatic Reference Counting). В итоге, вывод: автоматическое управление памятью более предпочтительно, т.к. оно более проработано, поддерживается мобильными устройствами и более привлекательно для новичков.

Итак, сегодня в Delphi (и я говорю про Delphi XE4) есть пять компиляторов: для Win32, Win64, MacOS, эмулятор iOS (компилирует в x86) и iOS (компилирует в ARM). Компиляторы для Win32, Win64, MacOS и эмулятор iOS являются классическими, а компилятор для iOS основан на новой архитектуре LLVM. Как я сказал выше, C++ Builder отличается тем, что компилятор для Win64 у него тоже является новым (LLVM).

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

Изменения в языке

Несмотря на то, что Delphi уже давно поддерживает несколько платформ, до сих пор язык Delphi не претерпел никаких хирургических вмешательств по отсечению старых возможностей. Компилятор для каждой новой платформы создавался полностью Borland/CodeGear/Embarcadero и педантично тащил за собой весь багаж обратной совместимости.

Сейчас ситуация несколько иная. Во-первых, необходимо сделать компилятор (front-end) из Паскаль кода в LLVM IR — что потребует тщательного воспроизведения всего накопленного багажа из обратной совместимости. Во-вторых, ввод нового компилятора совпадает с введением поддержки мобильных платформ. Перенос старого уже написанного кода на мобильную платформу, вероятно, и так потребует пересмотра. В-третьих, добавление новых платформ требует введения в язык новых возможностей. Частично они будут перекрывать старые. В языке будет несколько способов сделать одно и то же. Язык станет слишком сложным сам по себе, не говоря уже о сложностях изучения его для новичков. В четвёртых, уже сегодня в Delphi есть как избыточность (посмотрите, сколько есть в ней типов строк), так и несогласованность (сравните индексацию с 1 для строк, но с 0 — для списков и массивов).

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

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

Итак, современные (и будущие) изменения в языке Delphi заключаются в следующем:

  1. Строки:
    • всего один тип строк
    • индексируются с 0
    • «неизменяемые» строки (immutable strings)
  2. Улучшения классического механизма автоматического подсчёта ссылок:
    • Автоматические ссылки для объектов
    • Слабые (weak) ссылки
  3. Новые классы и процедуры в RTL для кросс-платформенного кода
  4. Отсутствие пакетов и DLL на некоторых платформах
  5. В будущем:
    • with — deprecated
    • object — deprecated
    • указатели — deprecated
    • ассемблер — deprecated

Строки

Больше всего изменений в новых версиях Delphi приходится на строки. Для этого есть несколько причин:

  • Упрощение модели строк (несколько типов строк)
  • Унификация (1-индексация)
  • Оптимизация (требования более слабых мобильных платформ)

Сохранение обратной совместимости со строками из времён Turbo Pascal/Delphi 1 слишком затратно как для разработчиков самой Delphi, так и для разработчиков на Delphi (особенно новичков).

Единый строковый тип

Сегодня в Delphi есть следующие типы строк:

  • Delphi-строки:
    • Родной ( string ) — псевдоним для UnicodeString
    • UnicodeString (счётчик ссылок, Unicode, длина, размер символа, нуль-терминированная)
    • AnsiString (счётчик ссылок, Ansi, длина, размер символа, нуль-терминированная)
    • AnsiString[ кодовая-страница ] (счётчик ссылок, кодовая страница, длина, размер символа, нуль-терминированная)
    • RawByteString (счётчик ссылок, длина, размер символа, нуль-терминированная)
  • Pascal-строки:
    • ShortString (Ansi-кодировка, 255 символов, счётчик длины в первом символе)
    • String[ число ] (Ansi-кодировка, менее 255 символов, счётчик длины в первом символе)
  • C-строки:
    • PChar — псевдоним для PWideChar
    • PAnsiChar (Ansi, нуль-терминированная)
    • PWideChar (Unicode, нуль-терминированная)
  • WideString ( BSTR из COM, Unicode, нуль-терминированная, счётчик длины, специальный API)

Если вы посмотрите на этот список, то заметите следующую вещь: всюду в вашей программе вы оперируете со строками типа string . Все прочие типы строк нужны вам исключительно для совместимости со сторонним кодом: вашим же старым кодом (AnsiString или Pascal-строки), ОС (нуль-терминированные или BSTR ) и т.п. Такой зоопарк не только вызывает путаницу (вопросы вида «в чём разница между WideString и UnicodeString ?»), но и весьма сложен для переноса на другие платформы (чему равен WideString на iOS?). Поэтому идея заключается в том, чтобы оставить один тип строк — самый удобный и универсальный. Гораздо лучше использовать не строковые типы (записи/классы) для коммуникации с внешним миром — так их семантика будет понятнее. А перегрузка операторов сделает безболезненным операции присваивания.

Именно поэтому на новых LLVM компиляторах iOS есть только тип string . Все прочие типы строк там не объявлены и при попытке ими воспользоваться сгенерируют вам ошибку вида «Undeclared identificator AnsiString». Новый тип string в целом равен UnicodeString (т.е. хранит данные строки в UTF-16, имеет счётчик ссылок и длины, а также поле кодовой страницы, которое перманентно равно CP_UTF16 = 1200 ($4B0), и поле размера символа, которое перманентно равно 2 байтам).

Однако сказанное не означает, что вы не сможете работать с данными строк других форматов — просто вы не сможете это делать со встроенными (native) типами данных. Например, предположим, вам нужно использовать текстовые данные в формате UTF-8. Вы можете использовать классы типа TTextReader или TEncoding (которые, кстати, тоже появились в Delphi довольно давно), например:
Этот простой код скрывает от вас всю работу с UTF-8 строками. А вот вариант с явным преобразованием:

Вам может потребоваться хранить строковые данные в других форматах в памяти (например, при вызове сторонних API функций) — в этом случае вам нужно использовать класс TEncoding и хранить строковые данные в (динамическом) массиве байтов ( TBytes ). При желании вы можете даже эмулировать поведение старого компилятора путём введения типов с перегрузкой операторов, например:
Реализация этого класса может использовать TEncoding для работы (конкретно — TUTF8Encoding ). Используя такую запись, вы можете продолжать использовать старый код вида:

0-индексируемые строки

Как известно, первый символ в любой строке Delphi имеет индекс 1, а не 0, как может ожидать любой программист, ранее не знакомый со строками в Delphi. Это называется 1-индексацией (или индексацией с единицы). 1-индексация строк усугубляется тем, что другие структуры в Delphi (динамические массивы, списки и т.п., а также не-Delphi строки) индексируются с нуля (используют 0-индексацию). Получается некоторая путаница и непривычные корректировки на +/-1 в коде по работе со строками.

Историческая справка: почему в Delphi строки индексируются с 1?
Delphi является наследником языка Pascal. В Паскале не использовались 0-терминированные строки из C. Вместо этого Паскаль использовал так называемые «короткие» строки: первый байт строки служил счётчиком символов (= «байтов» в Паскале) в строке. Таким образом, в отличие от строк C строки Паскаля могли хранить #0 внутри строки и очень быстро определять длину (не нужно было искать терминатор в строке, не было цикла), но были ограничены 255 символами (т.е. строка занимала максимум 256 байт вместе со счётчиком).

Соответственно, в Паскале строки технически индексировались с нуля, но нулевой символ отводился под счётчик длины строки, а данные строки начинались с символа №1. Т.е. данные строки индексировались с единицы.

Когда Delphi ввела длинные строки (AnsiString в Delphi 2), то, хотя у длинных строк уже не было счётчика длины в первом символе строки (теперь он хранился в скрытом заголовке строки), индексацию с 1 оставили по соображениям обратной совместимости — чтобы не пришлось переделывать уже написанный код, который работал со строками в предположении, что они индексируются с 1.

Таким образом строки в Delphi стали индексироваться с 1.

Совместно с введением одного единственного строкового типа решено было изменить и этот аспект поведения строк. Поскольку подобное изменение весьма значительно для языка, но не привязано к архитектуре компилятора, то было решено контролировать этот аспект директивой компилятора: $ZEROBASEDSTRINGS . Кстати, эта директива впервые появилась ещё в XE3. По умолчанию эта директива выключена в Delphi XE3, а в Delphi XE4 она выключена для Win32, Win64 и OSX и включена для iOS и эмулятора iOS. Поскольку эта опция контролируется директивой, то вы можете включить её для Delphi XE3 (чтобы начать миграцию раньше). Более того, вы можете выключить её для iOS, чтобы компилировать старый код.

На что нужно обратить внимание:

  • Внутренняя структура строк не меняется. Иными словами не существует такого понятия как «0-индексированная строка». Строка — это строка. Индексация — это лишь способ доступа к данным, он не влияет на сами данные. Т.е. вы можете смешивать в одном проекте модули, собранные с разными настройками. Более того, вы можете иметь разные настройки для разных функций в рамках одного модуля.
  • Все новые функции в Delphi (хэлпер TStringHelper , TStringBuilder ) используют новую семантику (0-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора.
  • Все классические функции RTL ( Copy , Pos , Delete и т.п.) всегда используют прежнюю семантику (1-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора. Тем не менее, Embarcadero рекомендуют не использовать старые RTL-функции (используйте TStringHelper и TStringBuilder ).

Другими словами, опция $ZEROBASEDSTRINGS влияет только на вычисление выражений вида StrVar[ число ] . Посмотрите на такой код:
В любых предыдущих версиях Delphi (XE2 и ниже), а также в XE3 и выше с выключенной опцией $ZEROBASEDSTRINGS вы получите ‘ HOllo foo ‘. Но если вы добавите <$ZEROBASEDSTRINGS ON>перед кодом (либо запустите его на iOS, где эта опция уже включена), то получите ‘ HeOlo foo ‘. Единственная разница между этими двумя кусками — способ вычисления S[2] : в первом случае вы обращаетесь ко второму элементу, который имеет индекс 2 (отсчёт с 1), во втором случае вы обращаетесь к третьему элементу, который имеет индекс 2 (отсчёт с 0).

Примечание: в предварительных обсуждениях релиза XE4 было несколько заблуждений относительно строк. Заметьте, что способ интерпретации выражения в квадратных скобках для строк вообще не зависит от структуры строки, а остаётся на усмотрение компилятора. В самом деле, вы и ранее использовали 1 как индекс для первого символа длинных строк, но как второй символ для коротких строк (первый символ занят под счётчик и имеет индекс 0). Т.е. строки остаются теми же самыми, меняется только способ вычисления компилятором выражения StrVar[ число ] . Вы не передаёте в функцию «0-индексированную строку», вы передаёте «просто строку». Это означает, что вы можете смешивать в одном проекте и модули функции, скомпилированные с разными настройками. Посмотрите на такой код:
По умолчанию, в Delphi XE4 этот код покажет 2/1/1/2 на Windows и 2/2/1/2 на iOS. И снова: единственное отличие — интерпретация выражения в квадратных скобочках. И снова: вы можете изменить поведение на любой платформе на обратное, используя $ZEROBASEDSTRINGS .

Если вы хотите написать универсальный код, который будет работать для обоих вариантов $ZEROBASEDSTRINGS , то вы можете определить константы, зависящие от значения Low(string) , которое будет равно 1 и 0 для <$ZEROBASEDSTRINGS OFF>и <$ZEROBASEDSTRINGS ON>, соответственно. Например: Этот код будет работать всегда одинаково, вне зависимости от настроек компилятора. А вот как вы можете работать с циклами: Low(S) возвращает 0 для 0-индексированной строки и 1 — для 1-индексированной. High(s) возвращает Length(S) — 1 для 0-индексированной строки и Length(S) — для 1-индексированной. В случае пустой строки Low , конечно же, возвращает всё то же значение, а High возвращает -1 или 0, соответственно. Вы можете передать тип вместо переменной в Low , но это не сработает для High .

Вместо Low и High вы можете использовать хэлпер для строк, который появился в Delphi XE3. Фактически, в Delphi XE3 появилась новая возможность: возможность добавлять методы любым встроенным типам данным, а не только записям и классам. Хотя синтаксис несколько необычен для Delphi: Кроме самой возможности в Delphi XE3 были введены и некоторые новые конструкции, использующие новую возможность. Среди них: TStringHelper — хэлпер для типа string . Он объявлен в модуле SysUtils и предоставляет методы вида Compare , Copy , IndexOf , Substring , Length , Insert , Join , Replace , Split и многие другие. Поэтому теперь вы можете написать: Заметьте, что все эти методы (включая индексированное свойство Chars ) используют индексацию с нуля вне зависимости от настроек компилятора.

Immutable-строки

Несмотря на то, что новый единый тип string по-прежнему эквивалентен бывшему UnicodeString , внутренняя реализация строк может быть изменена в будущем и/или на других мобильных платформах. Уже сейчас предполагается, что строки станут неизменяемыми (т.н. immutable-строки): это означает, что строку нельзя изменить когда она была создана. Этот аспект не влияет на операции типа конкатенации (сложения строк), потому что эти операции создают новую строку из каких-то других строк. Immutable-строки влияют на in-place операции вида S[1] := ‘A’; — такие операции «запрещены».

Ещё раз: сегодня строки по прежнему изменяемы в любых компиляторах (в том числе — для iOS). Конструкции вида S[1] := ‘A’; полностью разрешены (в том числе — для iOS). Тем не менее, в будущем этот аспект может быть ограничен.

Сегодня все компиляторы Delphi используют семантику copy-on-write (копирование-при-записи): если вы модифицируете строку, а она имеет счётчик ссылок больший 1, то строка копируется в новую, и изменения вносятся в копию, оставляя старую версию неизменной — так что все остальные (кто держит ссылку на строку) не увидят вашего изменения. Иными словами, вместо копирования строки изначально при присваивании, техника copy-on-write копирует строку позже — когда её необходимо изменить. Копирования может и не произойти, если вы не модифицируете строку. Внутренне это достигается (скрытыми) вызовами UniqueString для строк вида S[1] := ‘A’; . Разумеется, вам нужно вставлять вызовы UniqueString вручную, если вы работаете с содержимым строки напрямую (через указатели).

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

Уже сегодня вы можете найти потенциальные пробные места в вашем коде. Для этого вы можете включить подсказки компилятора директивой <$WARN IMMUTABLE_STRINGS ON>. С включенной опцией компилятор будет выдывать такие предупреждения:

[dcc32 Warning]: W1068 Modifying strings in place may not be supported in the future”

И снова: если вы используете в вашем коде только конкатенацию строк, то immutable-строки вас не коснутся — ни сейчас, ни в будущем (в самом деле, оптимизация конкатенации — это одна из целей для развития Delphi). Разработчики Delphi не ожидают, что конкатенация строк станет медленнее или будет запрещена в будущем. Только изменение индивидуальных символов (содержимого строки) может вызвать проблемы в будущем (а может и не вызвать).

Тем не менее, сегодня операция конкатенации может быть не самым оптимальным способом работы со строками на мобильной платформе. Вы можете знать, что в Delphi уже давно есть специализированный класс для построения строк: TStringBuilder . Несмотря на то, что этот класс присутствует в Delphi уже давно (начиная с Delphi 2009), он не пользуется популярностью. Почему? Посмотрите на такой код:
На Desktop-платформах подобный код даст следующие результаты:
Иными словами, на мощных платформах нет никакой выгоды от использования TStringBuilder , поскольку умный менеджер памяти (типа FastMM или даже встроенного в ОС) успешно выполняет ту же работу, что и TStringBuilder (работу по динамическому росту блоков памяти).

Но на мобильных платформах ситуация иная: более слабые платформы не имеют такого же сложного и умного менеджера памяти, как и Desktop-ы. Поэтому результат работы такого кода на iOS будет следующим:
В этом случае TStringBuilder почти в два раза быстрее простого сложения строк.

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

Улучшения классического механизма автоматического подсчёта ссылок

Delphi для iOS вводит в язык поддержку ARC (Automatic Reference Counting) — «автоматический подсчёт ссылок». ARC является улучшенным механизмом подсчёта ссылок, который существовал в Delphi со времён Delphi 2 — для строк, вариантов, динамических массивов и интерфейсов. Фактически, единственными данными, управляемыми вручную, в Delphi являлись объекты и указатели. И если указатели уже давно успешно вытесняются управляемыми аналогами, то объекты продолжали оставаться типами с ручным управлением, плодя бесконечные вложенные иерархии try-finally в вашем коде.

До сегодняшнего дня. Сегодня ARM компилятор Delphi вносит автоматическое управление временем жизни и в объекты.

Автоматические ссылки для объектов

ARC является механизмом автоматического учёта памяти. Часто ему противопоставляют реализацию автоматического учёта памяти из .NET, называемую (несколько ошибочно) сборкой мусора (garbage collection). Оба механизма служат одной цели, но делают это разными способами. Напомню, что менеджер памяти .NET периодически запускает подпрограмму очистки памяти, которая пытается найти блоки памяти (или группы блоков), на которые нет внешних ссылок. Здесь же видно, в чём отличие двух подходов: ARC 100% детерминирован — память освобождается всегда в один и тот же момент (когда счётчик ссылок падает до нуля), способ .NET может освобождать память позднее, чем она реально отпускается. Кроме того, освобождение памяти (и, следовательно, объектов) в ARC выполняется текущим же потоком, а не фоновым потоком-уборщиком, как это происходит в .NET. Однако, ARC всё ещё допускает возможность утечек памяти, если вы создадите циклическую ссылку (первый объект указывает на второй, а второй — на первый), в то время как .NET увидит два блока памяти, изолированные от остальных, и удалит их.

Примечание: хотя ARC реализован только в (LLVM) компиляторе для iOS, его эмуляция также доступна на (классическом) компиляторе «эмулятор iOS». ARC не доступен в компиляторах для Win32, Win64 и OSX.

Использовать ARC очень просто — вам практически не нужно думать об управлении памятью. В вашей практике вы постоянно использовали строки ( string ) и практически никогда не задумывались об управлении памяти для них. Точно так же вы теперь можете поступать и с объектами:
Ближайший аналог ARC для объектов — это работа с интерфейсами (interface) в Delphi. Если вы когда-либо работали с интерфейсами в Delphi, то теперь точно так же сможете работать и с обычными объектами.

Точно так же, как с интерфейсами (и любыми другими типами с автоматическим управлением памятью в Delphi), вы можете удалить ссылку преждевременно (до выхода переменной за область видимости) путём присвоения переменной значения nil :
Хотя строка » end » по прежнему будет содержать (скрытый) блок finally с очисткой MyObj — в этом варианте кода «магия» компилятора отработает вхолостую, поскольку вы сами освободили ссылку до выхода из подпрограммы. Разумеется, если метод DoSomething вызовет исключение, то строка с присвоением nil будет пропущена, и тогда объект, как и ранее, будет удалён из «подстилки» компилятора в строке » end «.

Заметьте, что в этих примерах отсутствуют явные блоки try-finally — и код при этом остаётся 100% корректным. Это благодаря тому, что блок try-finally теперь является скрытым. Теперь вам не нужно писать многоуровневые вложенные блоки try-finally ! Фактически, то, что делает сейчас ARC, эквивалентно такому коду (который, впрочем, вы и сами могли писать ранее вручную):

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

  • Использовать <$IFDEF AUTOREFCOUNT>, разделив код на два варианта.
  • Использовать классический подход с Free / FreeAndNil , не используя преимущества ARC. На ARC этот подход формально будет работать благодаря обратной совместимости, хотя его поведение может незначительно отличаться.

По первому пункту: новый компилятор предоставляет следующие (новые) символы условной компиляции (определения для компиляторов даны по состоянию на XE4):

Символ: Условие: Компиляторы:
NEXTGEN Новый компилятор dcciosarm, dccios32
AUTOREFCOUNT Доступен ARC dcciosarm, dccios32
CPUARM Для процессоров с архитектурой ARM dcciosarm
IOS Целевая платформа — iOS dcciosarm, dccios32
WEAKREF Компилятор может использовать слабые ссылки dcciosarm, dccios32

Примечание: все символы условной компиляции можно посмотреть здесь.

По второму пункту: разумеется, разработчики Delphi не могли просто «выбросить на свалку» базилионы написанных сторонними разработчиками строк кода на Delphi, объявив их «устаревшими» и «несовместимыми с новой моделью». К примеру, если рассмотреть такой классический код:
В классическом компиляторе, где объекты являются неуправляемыми типами данных, вызовы FreeAndNil , Destroy или Free безусловно удаляли существующий объект. В новых компиляторах с поддержкой ARC этот код будет работать немного иначе: вызовы FreeAndNil , Destroy и Free будут эквивалентны » := nil » (т.е. очистке ссылки). Иными словами, блок кода выше в компиляторе с ARC будет скомпилирован как:
Что является 100% рабочим и корректным кодом, пусть и не самым разумным и эффективным. Иными словами, старые вызовы FreeAndNil / Free / Destroy полностью допустимы и безопасны, хотя и бесполезны в компиляторах с ARC.

Однако это не означает, что вы сможете использовать весь свой старый код без модификаций. В старом коде у вас могут быть более сложные ситуации — например, несколько ссылок на один объект. С классическим компилятором висячая ссылка (вы удалили объект по одной ссылке, но остальные ссылки не были сброшены) ваш объект удаляется, но на него продолжают указывать ссылки. Это — допустимо, если вы не обращаетесь к объекту по висячим ссылкам. Но в новой модели эти висячие ссылки добавят «+1» к счётчику ссылок объекта. Таким образом, очистка ссылки вызовом FreeAndNil / Free / Destroy уменьшит счётчик, но не до 0. Т.е. объект удалён не будет. Само собой, это не означает утечки памяти — объект всё же будет удалён, но позже — когда удалится последняя (ранее «висячая») ссылка. Так что ваш код может работать и как ранее (только изменится картина выделения/освобождения памяти), но, быть может, вам необходимо очистить объект до наступления другого события (такого, как выгрузка библиотеки, из которой объект и получен). В этом случае ваш код может вылететь. Решение заключается в правиле, которому не грех было бы следовать и ранее (ещё с классическим компилятором): не оставляйте висячих ссылок. Очищайте все ссылки на объект при его удалении.


Альтернативным решением задачи гарантированного вызова деструктора может быть вызов (нового) метода DisposeOf :
Метод DisposeOf безусловно вызывает деструктор — даже несмотря на существующие ссылки на объект. После такого вызова деструктора объект переходит в состояние «зомби» («zombie state» или «disposed state») — для него был вызван деструктор, объект был очищен, но память для него ещё не была освобождена. Вы можете узнать состояние объекта через свойство Disposed — это аналог Assigned для объектов из классического компилятора.

Разница между вызовами FreeAndNil / Free и DisposeOf заключается в ваших намерениях: вызов FreeAndNil / Free отсоединяет ссылку, но не означает немедленного удаления объекта (он может быть удалён сейчас, но может быть удалён и позднее), а вызов DisposeOf всегда безусловно удаляет объект, даже если на него есть ссылки.

Примечание: «зомби»-объект никак не защищается от возможного ошибочного доступа к нему. Вы можете прочитать/записать свойство, вызывать методы (как обычные, так и виртуальные) — все эти операции будут успешными, но будут оперировать на уже очищенном объекте. И хотя это не приведёт к Access Violation, как в классическом компиляторе с висячими ссылками (потому что память под «зомби» объект всегда гарантировано выделена), но все структуры данных объекта уже были очищены деструктором, что может привести к неожиданному поведению. Всегда проверяйте статус объекта вызовом Disposed , если вы удаляете объект вручную. Кроме того, вы можете проверить доступность объекта в самих методах объекта вызовом protected -метода CheckDisposed — это некий аналог Assert(Disposed); .

Заметьте, что старый Assigned вместе с FreeAndNil больше не имеют смысла в новой архитектуре, потому что объект всегда гарантировано существует (пусть даже и как зомби), пока на него есть хоть одна ссылка — это отличается от классической модели, где вам приходилось записывать в ссылку nil , чтобы указать на уже удалённый объект. (Хотя, конечно, вы можете продолжать использовать Assigned , если вы очищаете ссылки на объекты до их выхода из области видимости.)

К счастью, вам не нужно увлекаться <$IFDEF AUTOREFCOUNT>, потому что и DisposeOf и Disposed доступны и в классических компиляторах (начиная с XE4, конечно же). Код выше будет полностью работоспособен и в Win32, где вызов DisposeOf просто вызывает Free , ну а Disposed всегда возвращает False , поскольку в классическом компиляторе нет состояния «зомби». Поэтому, если у вас есть старый код и вы хотите точно такого же поведения (т.е. удалять объект сразу, а не когда уйдёт последняя висячая ссылка на него), то вы можете просто заменить вызовы FreeAndNil / Free / Destroy на вызов DisposeOf . К несчастью, вместо двух состояний «есть объект»/»нет объекта» у вас теперь появляется три состояния: «есть объект»/»зомби»/»нет объекта» — что, впрочем, не сильно отличается от бывшего «есть объект»/»висячая ссылка — непонятно, есть объект или нет»/»нет объекта» — которое в классическом компиляторе вы должны были сводить к «есть объект»/»нет объекта». В связи с этим, вам может пригодится такая подпрограмма:
Эту функцию можно использовать во всех местах, где вы раньше использовали if Assigned(Obj) then — замените их на if ValidObject(Obj) then .

Примечание: деструктор в ARC по прежнему называется Destroy , но он заблокирован для прямого вызова (помещением в секцию protected ). Поэтому:

  1. Добавьте <$IFDEF AUTOREFCOUNT>protected <$ENDIF>перед каждым destructor Destroy; override;
  2. Замените все внешние вызовы Destroy (если они вдруг у вас есть) на FreeAndNil / Free или DisposeOf — смотря по тому, согласны ли вы с отложенным удалением объекта или вам нужно немедленное удаление.

Суммируя сказанное, вот современная реализация TObject (показан только код, имеющий отношение к циклу создание-удаления объектов):

Слабые (weak) ссылки

Однако поддержка ARC в Delphi касается не только расширением действия счётчиков ссылок на классы/объекты, но и поддержки слабых (weak) ссылок. Слабые ссылки предназначены для решения проблемы циклических ссылок. Наиболее типичный случай возникновения циклических ссылок: контейнер-коллекция, в котором его элементы содержат ссылки на него самого (как на контейнер-владелец). В классической модели ссылок из Delphi подобная конструкция порождает утечку из-за наличия циклической ссылки.

Здесь на сцену выходят слабые ссылки. Слабая ссылка — это ссылка на объект, которая не приводит к изменению счётчика ссылок. Иными словами, при присвоении объекта в переменную со слабой ссылкой не происходит увеличение счётчика ссылок объекта на единицу. Аналогично, при очистке слабой ссылки не происходит уменьшение счётчика объекта на единицу. Создать слабую ссылку очень просто — достаточно пометить переменную атрибутом [weak] , например:
В этом примере поле FOwnedBy является слабой ссылкой, потому что оно помечено атрибутом [weak] . Это означает, что присвоение этому полю не увеличивает счётчик ссылок присваевомого объекта, а его очистка — не уменьшает счётчик ссылок объекта. Таким образом, создание экземпляра TMyComplexClass не приведёт к утечке памяти, несмотря на наличие циклической ссылки — благодаря тому, что одна из ссылок в составе циклической ссылки является слабой.

Вы можете увидеть, что атрибут [weak] используется и в коде самой Delphi, например:

Примечание: вы можете использовать атрибут [weak] и в классических компиляторах, но он будет игнорироваться, поскольку в этих компиляторах нет ARC. Таким образом, если вы пишете универсальный исходный код — вам необходимо как помечать переменные атрибутом [weak] , так и использовать FreeAndNil / Free (использование которых допускается в компиляторах с ARC).

Вы также не можете проверить статус объекта по слабой ссылке. Чтобы проверить статус объекта, вам сначала нужно присвоить объект в обычную переменную, например:

Диагностика с ARC

Использование ARC упрощает работу с памятью и снижает риск утечек памяти/ресурсов в вашем коде, но поскольку всё же существует вероятность создать циклическую ссылку, то ваш код всё ещё не полностью защищён от утечек памяти.

С целью отладки вы можете использовать свойство RefCount , чтобы узнать число живых ссылок на объект. Не следует использовать это свойство для реализации логики программы. Кроме того, вы можете (крайне редко) использовать __ObjAddRef и __ObjRelease для ручного управления счётчиком ссылок — например, для записи объекта в неуправляемую переменную-указатель (к примеру, свойства типа Tag / Data ). Этот приём допустимо использовать в логике кода, хотя его и нужно избегать (предпочтительнее: создание наследника с полем нужного типа).

Вы можете проверить наличие циклических ссылок с помощью функции CheckForCycles (модуль Classes ):
Как и выше, эта функция служит для диагностики, её не следует использовать в финальной сборке программы/для реализации логики самой программы. Используйте её только для отладки.

Заметьте, что по аналогии со строками и интерфейсами ARC с объектами является потокобезопасным: при работе со счётчиком ссылок используются атомарные interlocked-операции. Заметьте, что это не означает автоматической потокобезопасности самих объектов.

Обратите внимание, что на мобильных платформах Delphi использует функции операционной системы в качестве штатного менеджера памяти. Иными словами, FastMM — штука чрезмерно сложна для мобильной платформы и, более того, написанная на x86-ассемблере (иными словами: непереносимая). Поэтому возможности Delphi по диагностике утечек памяти будут недоступны. Вы можете использовать инструментарий целевой ОС или использовать сторонние фильтры.

Новые классы и процедуры в RTL для кросс-платформенного кода

В целом вы должны избегать прямых платформенных вызовов (т.е. функций Windows/Mac/iOS API) и использовать, предлагаемые Embarcadero обёртки-переходники. Конечно же, вы также должны как чумы избегать ассемблера и, желательно, не использовать указатели.

Например, Embarcadero предлагает вам модуль IOUtils. Он доступен, начиная с Delphi 2010. Вы можете прочитать про него здесь. Как можно догадаться, этот модуль предоставляет вам кросс-платформенные возможности для работы с файлами. В нём есть классы TDirectory , TPath и TFile — для работы с каталогами, именами файлов и файлами соответственно.

К примеру, вы можете получить доступ к папке «Documents» на мобильном устройстве так же, как вы получаете доступ к папке Application Data в Windows:

А вот как вы можете искать файлы: этот код считывает подпапки заданной папки, а затем считывает файлы в найденных подпапках:

Библиотеки и пакеты

К сожалению, одна из древнейших возможностей Delphi — использование пакетов времени выполнения (run-time packages, BPL) и, более обще, DLL — не поддерживается на платформе iOS. Пакеты и библиотеки представлены DLL на Windows, dylib на MacOS и so (shared object) на Linux. Они позволяют вам создавать модульные приложения. Но на iOS приложение не может устанавливать библиотеки — это может делать только сама Apple, а iOS приложения обязаны быть монолитными программами.

Тем не менее, компилятор Delphi умеет распознавать статические ссылки на DLL (например, на midas.dll) и внедрять их в приложение статически, а не как отдельные библиотеки.

«Плохие» конструкции

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

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

Object (старые объекты Паскаля) устарели много лет назад. Замените их на записи (record).

Ассемблерный код уже подвергся ограничению при переходе на Win64 (нельзя внедрять ассемблерный код в середину функции), а в будущем он может быть ограничен ещё больше. Уже сейчас он не поддерживается компилятором iOS. Ассемблерный код привязан к конкретной платформе и не портируется на другую платформу. Постарайтесь не использовать его.

Прямой доступ к указателям есть не на всех платформах. Хотя даже сегодня использование указателей не поощряется (как подверженное ошибкам), но они всё ещё полностью поддерживаются всеми компиляторами Delphi. Только надо иметь в виду, что в будущем их использование может быть ограничено или вовсе отсутствовать для некоторых платформ. Уже сейчас указатели удаляются из языка в пользу ARC (к примеру, в Delphi для iOS отсутствует модуль System.Contnrs , поскольку он основан на TList с указателями). Поэтому если у вас есть выбор, использовать указатели или безопасный аналог — не используйте указатели.

К примеру, TList и TStringList являются своеобразными «швейцарскими ножами»: они используются как универсальный контейнер на все случаи жизни, благодаря способности хранить произвольные ссылки (для TStringList — через свойство Objects ). Но новые версии Delphi поддерживают дженерики (generics) и имеют более узкоспециализированные классы — и их использование будет предпочтительнее по двум причинам: меньше ошибок (нет приведений типов) и быстрее выполнение (может использоваться хэш-таблица).

Рассмотрим такой код с двумя идентичными списками:
Списки заполняются случайными (но идентичными для обоих списков) значениями в цикле:
Попробуем получить каждый объект в обоих списках по его имени (ключу). Оба списка содержат идентичный набор данных, а имена ключей (объектов) хранятся в отельном списке ( sList ):
Сколько времени займёт поиск в отсортированном списке строк (который использует двоичный поиск в случае отсортированного списка) по сравнению со словарём (который использует хэш-ключи)?
Результат работы обоих вариантов кода идентичен (предполагая, что на вход поступил один и тот же набор данных), но скорость выполнения значительно отличается: TStringList работает в четыре раза медленнее словаря (пример дан для миллиона записей).

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

Заключение

С выходом первого компилятора Delphi для ARM, основанного на архитектуре LLVM, язык Delphi переживает важный переходный период. Хотя разработчиками Delphi были предприняты усилия для поддержания обратной совместимости, разработчикам рекомендуется использовать новые возможности языка и двигаться вперед.

Описанные в этой статье возможности языка (в частности — поддержка ARC) будут формировать будущее Delphi. Эти изменения частично обусловлены поддержкой новой платформы, а частично предназначены для исправления некоторых «плохих» мест в языке Delphi.

Суммирующая табличка по компиляторам для Delphi XE4 (компиляторы для C++ Builder не показаны, за исключением Win64):

Dynamic Form Creation

I create my forms at runtime something like this:

But what is the difference though in using any of these:

They all seem to work the same from what I can see but which is correct, or are they all correct, which is generally the best one to use?

Appreciate your comments thanks :)

3 Answers 3

The TForm.Create takes an Owner as parameter.

In your first example, AboutForm is the owner. Which obviously is a bad idea, since it’s not created yet.

When Self is the parameter, the instance that makes the call is the owner.
When Application is the parameter, the Application is the owner.
When nil is the parameter, the AboutForm doen not have a owner. That is all fine, but in those cases you must remember to free the form yourself.

When you do pass in a owner, you actually don’t need to free i explicitly. The owner will free it when the owner is freed.

This is how your code should look like:

when you create the Form dynamically, that is the form is available form. You have to Pass the owner of the form.

.. So in your case

  1. Self
  2. Nil
  3. Application

is the owner of the for AboutForm (as you already may know it)

But what is the difference though in using any of these:

nil — specifies that no object owns the form — and therefore a developer (you) is responsible for freeing the created form (by calling myForm.Free when you no longer need the form)

Self — specifies the object in which the method is called. If, for example, you are creating a new instance of a TMyForm form from inside a Button’s OnClick handler (where this button is placed on a MainForm) — self refers to «MainForm». Thus, when the MainForm is freed — it will also free «MyForm».

Application — specifies a global TApplication type variable created when you run your application. «Application» encapsulates your application as well as providing many functions that occur in the background of the program.

a)Formx.Create(Application) -> Form resources will be freed when application is terminated

b)Formx.Create(Self) -> Form resources are freed when the owner object is destroyed (if Self is a descendand of TComponent)

c)Formx.Create(nil) -> you are responsible for freeing the form.

a is used by delphi when a form is autocreated

b is handy for a main form that has several child windows that need to close when the main form is closed

c is handy for showing a dialog window

For freeing you can do this ACtion := caFree onclose of the form.

Разработка компонентов в среде Delphi

Все компоненты Delphi являются частью иерархии, которая называется Visual Component Library (VCL). Общим предком всех компонентов является класс TComponent (рис. 9.1.1), в котором собран минимальный набор общих для всех компонентов Delphi свойств.

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

csDesigning компонент находится в режиме проектирования
sDestroyingKOMnoHeHT сейчас будет разрушен;
csLoading компонент загружается из файла формы;
csReading компонент считывает значения из файла формы;
csWriting компонент записывает значения своих свойств в поток;
csUpdating компонент вносит изменения, чтобы отразить изменения в родительской форме.

Класс TComponent вводит концепцию принадлежности. Каждый компонент имеет свойство Owner (владелец), ссылающееся на другой компонент как на своего владельца. В свою очередь, компоненту могут принадлежать другие компоненты, ссылки на которые хранятся в свойстве Components. Конструктор ком­понента принимает один параметр, который используется для задания владельца компонента. Если передаваемый владелец су­ществует, то новый компонент добавляется к списку Components владельца. Свойство Components обеспечивает автоматическое разрушение компонентов, принадлежащих владельцу. Свойст­во ComponentCount показывает количество принадлежащих компонентов, a Componentlndex — номер компонента в массиве Components.

В классе TComponent определено большое количество мето­дов. Наибольший интерес представляет метод Notification. Он вызывается всегда, когда компонент вставляется или удаляется из списка Components владельца. Владелец посылает уведомле­ние каждому члену списка Components. Этот метод переопределя­ется в порождаемых классах для того, чтобы обеспечить действи­тельность ссылок компонента на другие компоненты. Например, при удалении компонента Tablel с формы свойство DataSet компонента DataSourcel, равное Tablel, устанавливается в Nil.

Процесс разработки компонента включает пять этапов:

создание модуля компонента;

добавление в новый компонент свойств, методов и событий;

регистрацию компонента в среде Delphi;

На рис. 9.1.1 изображены базовые классы, формирующие структуру VCL. В самом верху расположен TObject, который является предком для всех классов в Object Pascal. От него про­исходит TPersistent, обеспечивающий методы, необходимые для создания потоковых объектов. Потоковый объект — объект, ко­торый может запоминаться в потоке. Поток представляет собой объект, способный хранить двоичные данные (файлы). Поскольку Delphi реализует файлы форм, используя потоки, то TComponent порождается от TPersistent, предоставляя всем компонентам способность сохраняться в файле формы.

Класс TComponent представляет собой вершину иерархии компонентов и является первым из четырех базовых классов, используемых для создания новых компонентов. Прямые по­томки TComponent — невизуальные компоненты.

Вершину иерархии визуальных компонентов представляет класс TControl.

Класс TControl вводит понятие родительских элементов управ­ления (parent control). Свойство Parent является окном, кото­рое содержит элемент управления. Например, если компонент Panel 1 содержит Button 1, то свойство Parent компонента Button 1 равно Panel 1.

Свойство ControlStyle определяет различные стили, приме­нимые только к визуальным компонентам, например:

csAcceptControls элемент управления становится родителем любых элементов управления, помещенных на него во время проектирования. Применим только к оконным элементам управления;
csCaptureMouse элемент управления перехватываетсобытия мыши;
сsFrames элемент управления имеет рамку;
csSetCaption свойства Caption и Text элемента управления (если не заданы явно) устанавливаются так, чтобы совпадать со свойством Name;
csOpaque элемент управления скрывает все элементы позади себя.

В классе TControl определено большинство свойств, использу­емых визуальными компонентами: свойства позиционирования (Align, Left, Top, Height, Width), свойства клиентской области (ClientHeight, ClientWidth), свойства внешнего вида (Color, Enabled, Font, ShowHint, Visible), строковые свойства (Caption, Name, Text, Hint), свойства мыши (Cursor, DragCursor, DragKind, DragMode).

Кроме того, класс TControl реализует методы диспетчеризации событий.

Все визуальные компоненты подразделяют на графические элементы управления и оконные элементы управления. Каж­дый тип представляет свою иерархию классов, происходящую соответственно от TGraphicControl и TWinControl. Главная раз­ница между этими типами компонент состоит в том, что графи­ческие компоненты не поддерживают идентификатор окна, и, соответственно, не могут принять фокус ввода.

Оконные компоненты далее разбиваются на две категории. Прямые потомки TWinControl являются оболочками вокруг су­ществующих элементов управления, реализованных в Windows (например, TEdit, TButton, и др.) и, следовательно, знают, как себя рисовать.

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

9.1.2. Класс TGraphicControl

Класс TGraphicControl является базовым для компонентов, которые не нуждаются в получении фокуса ввода и не служат в качестве родительских для других элементов управления (эти функции требуют наличия идентификатора окна).

По умолчанию объекты TGraphicControl не имеют собствен­ного визуального отображения, но для наследников обеспечи­ваются виртуальный метод Paint (вызывается всегда, когда элемент управления должен быть нарисован) и свойство Canvas (используется как «поверхность» для рисования).

Класс TWinControl используется как базовый для создания компонентов, инкапсулирующих соответствующие оконные эле­менты управления Windows, которые сами себя рисуют.

Класс TWinControl обеспечивает свойство Handle, являюще­еся ссылкой на идентификатор окна базового элемента управ­ления. Кроме этого свойства класс реализует свойства, методы и события, поддерживающие клавиатурные события и измене­ния фокуса:

свойства фокуса TabStop, TabOrder;
свойства внешнего вида Ctl3D, Showing;
методы фокуса CanFocus, Focused;
методы выравнивания AlignControl, EnableAlign, Re Align;
оконные методы CreateWnd, CreateParam, RecreateWnd, CreateWindowHandle, DestroyWnd;
события фокуса OnEnter, OnExit;
события клавиатуры OnKeyDown, OnKeyPress, OnKeyUp.

Создание любого потомка этого класса начинается с вызова ме­тода CreateWnd, который вначале вызывает CreateParams для инициализации записи параметров создания окна, а затем вызыва­ет CreateWindowHandle для создания реального идентификатора окна, использующего запись параметров. Затем CreateWnd настра­ивает размеры окна и устанавливает шрифт элемента управления.

9.1.4. Класс TCustomControl

Класс TCustomControl представляет собой комбинацию клас­сов TWinControl и TGraphicControl. Являясь прямым потомком класса TWinControl, TCustomControl наследует способность управления идентификатором окна и всеми сопутствующими возможностями. Кроме этого, как и класс TGraphicControl, класс TCustomControl обеспечивает потомков виртуальным ме­тодом Paint, ассоциированным со свойством Canvas.

Таким образом, в зависимости от того, какой компонент бу­дет исходным (базовым) для создания нового класса, можно выделить 4 случая:

создание Windows-элемента управления (TWinControl);

создание графического элемента управления (TGraphic-Control);

создание нового элемента управления (TCustomControl); О создание невизуального компонента (TComponent).

9.2. Создание модуля компонента и тестового приложения

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

Выполните команду File/ New. / Component или Component/ New Component.

В диалоговом окне New Component (рис. 9.2.1.) установите основные параметры создания компонента: Ancestor type (имя класса-предка), Class Name (имя класса компонента), Palette Page (вкладка палитры, на которой должен отображаться ком­понент) и Unit file name (имя модуля компонента).

После щелчка на кнопке ОК будет сгенерирован каркас но­вого класса.

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

Упражнение 9.2.1. Разработайте новый компонент, который объединяет компоненты TEdit и TLabel. Компонент Label рас­полагается выше поля редактирования (TEdit). При перемеще­нии поля редактирования TLabel следует за ним. При удалении поля редактирования TLabel также удаляется.

В качестве предка класса нового компонента используем TEdit.

Выполните команду Component/ New component. Установите следующие значения параметров окна: Ancestor type TEdit

Class Name TLabelEdit

Palette Page Test

Unit file name . \LabelEdit\LabelEdit.pas

Щелкните на кнопке ОК, автоматически будет сгенерирован следующий код:

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type

В модуле описан каркас нового класса и написана процедура регистрации компонента (Register), которая помещает его на страницу Test. Сохраните файл модуля компонента.

Разработка тестового приложения

Создайте новый проект. Сохраните его файлы в папке . \LabelEdit: файл модуля — под именем Main.pas, файл про­екта — Test Application, dpr.

Добавьте имя модуля разрабатываемого компонента в раздел Uses формы тестового приложения:

В общедоступный раздел класса TForml добавьте поле

В обработчике события OnCreate формы динамически со­здайте новый компонент:

procedure TForml.FormCreate(Sender: TObject);

Сохраните файлы проекта.

Эксперимент. Убедитесь, что при запуске в левом верхнем углу формы появляется окно редактирования. ♦

9.3. Добавление свойств, методов и событий

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

Добавление свойства происходит в три этапа.

1. Создание внутреннего поля класса для хранения значения свойства.

2. Описание и разработка методов доступа к значению свойства.

3. Описание свойства.

В классе TControl свойства Caption/Text, Parent и Hint опре­деляются так:

TControl = class (TComponent)

function IsCaptionStored: Boolean;

function IsHintStored: Boolean;

procedure SetText(const Value: TCaption);

property Caption: TCaption read GetText write SetText stored IsCaptionStored;

property Text: TCaption read GetText write SetText;

property Parent: TWinControl read FParent write SetParent;

property Hint: string read FHint write FHint stored IsHintStored;

Объявление свойства имеет следующий синтаксис: property : тип определители;

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

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

Для указания метода, который будет использоваться для осуществления выборки значения свойства, используется ди­ректива read. Метод должен быть функцией, чей возвращае­мый тип является тем же самым, что и тип свойства.

Однако вместо метода доступа для чтения можно указать внутреннее поле хранения данных, как, например, при описа­нии свойств Hint и Parent. Подобная форма записи приводит к тому, что значение свойства извлекается прямо из внутреннего поля данных.

За спецификацией метода чтения следует определитель ме­тода записи, директива write определяет, какой метод будет ис­пользоваться для присвоения свойству значения. Метод должен быть процедурой, имеющей единственный параметр, тип кото­рого должен совпадать с типом свойства.

При обращении к значению свойства происходит перена­правление на соответствующий метод. Например, оператор s : =Editl. Text; автоматически будет преобразован в оператор s : =Editl. GetText; а оператор Editl. Text: =’ Test’ — в опе­ратор Editl.Text(‘Test’).

Описание свойства должно содержать определитель read или write или сразу оба. Если описание свойства включает в себя только определитель read, то оно является свойством только для чтения. В свою очередь, свойство, чье описание включает в себя только определитель write, является свойством только для записи. При присвоении свойству, определенному с директивой только для чтения, какого-либо значения или при использова­нии в выражении свойства с директивой только для записи все­гда возникает ошибка.

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

Когда программист использует Инспектор объектов для измене­ния свойств формы или свойств компонентов, то результирующие изменения заносятся в файл формы. Файлы форм представляют собой файлы ресурсов Windows, и когда приложение запускается, то описание формы подгружается из этого файла. Для определения того, что должно сохраняться в файле формы, служат специфика­торы памяти — необязательные директивы stored, default и node-fault. Эти директивы влияют на информацию о типе во время вы­полнения, генерируемую для свойств published.

Директива stored управляет тем, будет или нет свойство дейст­вительно запоминаться в файле формы. За директивой stored дол­жны следовать либо константы True или False, либо имя поля, имеющего тип Boolean, либо имя метода, у которого нет парамет­ров, и возвращающего значение типа Boolean. Например,

property Hint: string read FHint write FHint stored IsHintStored;

Если свойство не содержит директиву stored, то оно рассмат­ривается как содержащее ее с параметром True.

Директивы default и nodefault управляют значениями свой­ства по умолчанию. За директивой default должна следовать константа того же типа, что и свойство, например:

property Tag: Longint read FTag write FTag default 0 ;

Чтобы перекрыть наследуемое значение default без указания нового значения, используется директива nodefault. Директи­вы default и nodefault работают только с порядковыми типами и множествами, нижняя и верхняя границы которых лежат в промежутке от 0 до 31. Если такое свойство описано без дирек­тив default и nodefault, то оно рассматривается как с директи­вой nodefault. Для вещественных типов, указателей и строк значение после директивы default может быть только О, NIL и

(пустая строка) соответственно.

Когда Delphi сохраняет компонент, то просматриваются спе­цификаторы памяти published свойств компонента. Если значе­ние текущего свойства отличается от default значения (или ди­ректива default отсутствует) и параметр stored равен True, то значение свойства сохраняется, иначе свойство не сохраняется.

Спецификаторы памяти не поддерживаются свойствами-мас­сивами, а директива default при описании свойства-массива имеет другое назначение.

Простые свойства — это числовые, строковые и символьные свойства. Они могут непосредственно редактироваться в Инс­пекторе объектов и не требуют специальных методов доступа.

Рассмотрим создание простого свойства Color, описанного в классе TContol (модуль controls.pas):

TControl = class (TComponent)

function IsColorStored: Boolean;

procedure SetColor(Value: TColor);

property Color: TColor read FColor write SetColor stored IsColorStored default clWindow;

function TControl.IsColorStored: Boolean;

Result := not ParentColor;

procedure TControl.SetColor (Value: TColor);

if FColor <> Value then

Perform(CM_COLORCHANGED, 0, 0) ;

9.3.2. Свойства перечислимого типа

Определенные пользователем перечислимые и логические свойства можно редактировать в окне инспектора объектов, вы­бирая подходящее значение свойства в раскрывающемся списке. Рассмотрим создание свойства перечислимого типа на при­мере компонента Shape (модуль extctrls.pas).

TShapeType = (stRectangle, stSquare, stRoundRect, stRoundSquare, stEllipse, stCircle);

procedure SetShape(Value: TShapeType);

property Shape: TShapeType read FShape write SetShape

if FShape <> Value then

9.3.3. Свойства типа множества

Свойство типа множества при редактировании в окне Инспек­тора объектов выглядит так же, как множество, определенное синтаксисом языка Pascal. Простейший способ его отредактиро­вать — развернуть свойство в Инспекторе объектов, в результате каждый его элемент станет отдельным логическим значением.

При создании свойства типа множества нужно создать соот­ветствующий тип, описать методы доступа, после чего описать само свойство. В модуле Controls.pas свойсво Align описано сле­дующим образом:

TAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient);

TAlignSet = set of TAlign; TControl = class(TComponent)

procedure SetAlign(Value: TAlign);

property Align: TAlign read FAlign write SetAlign default alNone;

procedure TControl.SetAlign(Value: TAlign);

var OldAlign: TAlign;

if FAlign <> Value then

if not (csLoading in ComponentState) and

(not (csDesigning in ComponentState) or (Parent <> NIL))

if ((OldAlign in [alTop, alBottom])=(Value in [alRight, alLeft])) and not (OldAlign in [alNone, alClient]) and not (Value in [alNone, alClient]) then SetBounds(Left, Top, Height, Width)

в соответствии со значением свойства Align >

Свойства могут являться объектами или другими компонен­тами. Например, у компонента Shape есть свойства-объекты Brush и Реп. Когда свойство является объектом, то оно может быть развернуто в окне инспектора так, чтобы его собственные свойства также могли быть модифицированы. Свойства-объек­ты должны быть потомками класса TPersistent, чтобы их свой­ства, объявленные в разделе published, могли быть записаны в поток данных и отображены в инспекторе объектов.

Для определения объектного свойства компонента необходимо сначала определить объект, который будет использоваться в каче­стве типа свойства. В модуле graphics.pas описан класс TBrush:

procedure GetData(var BrushData: TBrushData);

procedure SetData(const BrushData: TBrushData);

function GetBitmap: TBitmap;

procedure SetBitmap(Value: TBitmap);

function GetColor: TColor;

procedure SetColor(Value: TColor);

function GetHandle: HBrush.;

procedure SetHandle(Value: HBrush);

function GetStyle: TBrushStyle;

procedure SetStyle(Value: TBrushStyle);

constructor Create; destructor Destroy; override;

procedure Assign(Source: TPersistent); override;

property Bitmap: TBitmap read GetBitmap write SetBitmap;


property Handle: HBrush read GetHandle write SetHandle;

property Color: TColor read GetColor write SetColor

property Style: TBrushStyle read GetStyle write SetStyle

Метод Assign предназначен для копирования значения свойств экземпляра TBrush:

Директива Delphi $ Message на основе условия

Я хочу генерировать фатальную ошибку компилятора, используя директиву <$Message Fatal ''>, но основанную на значении поля. Например:

Но это не работает.

Я сделал ошибку в коде? Или есть лучший способ использовать условную директиву сообщений?

Вы не можете использовать Pascal, if компилятор все еще компилирует все ветки. Вместо этого вы должны использовать условную директиву, такую как <$IF>.

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

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

Поле, которое вы тестируете, может быть изменено во время выполнения.

Сообщения компилятора генерируются во время компиляции. Невозможно создать сообщение об ошибке времени компиляции на основе условий времени выполнения.

У вас есть два варианта:

Используйте ASSERT() чтобы проверить значение поля во время выполнения и вызвать исключение, если обнаружено, что это условие было нарушено.

Замените поле символом, который можно установить и протестировать с помощью директив компилятора.

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

Подход ASSERT

И ваш оператор if и необходимость вызова EXIT объединяются в один вызов ASSERT() :

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

В противном случае ошибка утверждения предупреждает разработчика (или если вы небрежны, пользователь) к ошибке.

Подход символов компилятора

Либо используя параметр проекта, либо некоторую условную компиляцию в вашем устройстве или подходящий файл include, определите символ FILELISTMODE .

Затем ваш оператор if заменяется тестом на определение этого символа, но по-прежнему нет необходимости вызывать EXIT поскольку компиляция просто не срабатывает немедленно, если символ не определен:

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

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

Директивы процедур и функций

передача параметров в процедуре или функции осуществляется справа налево

это что получается же

что параметры будут присвоены в процедуре в порядке справа налево?

Для чего такая ересь? Это выигрывание процессорного времени? Что вперед обработаются z,y,x,d,c,b,a вот в такой последовательности.

20.07.2011, 13:29

Использование процедур и функций
Даны три произвольных массива Е, S, С размера n, m, k соответственно. Найти среднее геометрическое.

Использование процедур и функций
Использование процедур и функций. Дан интервал (с,d) и два произвольных массива Q, W размера n, k.

Отличия процедур и функций
скажите плз что такое процедуры и функции, и чем они отличаются

Описание процедур и функций
кто может помочь описать в программном коде для чего нужна эта процедура и т.д

20.07.2011, 16:11 2

cdecl, stdcall, pascal, safecall, fastcall и пр. — это соглашения по вызову процедур. Эти соглашения определяют различные правила вызова: как будут передаваться параметры — через стек, через регистры, через динамическую память, кто ответственный за очистку стека — вызывающая или вызываемая программа и др.

cdecl — параметры помещаются в стек в последовательности справа-налево. Таким образом, при этом получится так, что самый правый параметр окажется на дне стека, а самый первый слева параметр — на вершине стека. Соответственно, вызванная программа будет извлекать из стека эти значения в обратном порядке — сначала будет извлечён тот параметр, который находится на вершине стека — т. е. тот, который в списке параметров объявлен первым. Затем, из стека будет извлечён второй слева параметр и т. д. до самого правого параметра, который лежит на дне стека.
Ответственной за очистку стека является вызывающая программа.
Соглашение cdecl применяется в качестве основного в языке Си и др.

stdcall — тоже самое что и cdecl, но ответственной за очистку стека является вызываемая программа. Это соглашение является основным для API функций Windows.

cdecl и stdcall позволяют передавать произвольное количество параметров — потому что вызывающая программа всегда извлекает параметры из стека в том порядке, в котором они перечислены в спецификации процедуры. И извлечение можно продолжать до момента, когда будет достигнуто дно стека. — Т. е. признаком того, что все параметры извлечены является достижение дна стека. Это позволяет вызывать функции, которые могут содержать произвольное количество параметров.

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

Спецификация pascal применяется, как основная, в языках Pascal, Delphi и в некоторых других языках. Эта спецификация считается скоростной. Но она не позволяет вызывать функции с произвольным количеством параметров. — По причине того, что вызванная программа первым из стека извлекает тот параметр, который в спецификации функции объявлен крайним справа. — Т. е., надо точно знать общее количество параметров, тогда будет соблюдено точное соответствие формальных параметров и извлекаемых из стека фактических параметров, объявленных в спецификации процедуры.

safecall — применяется в технологии COM.
fastcall — передача параметров через регистры.
Есть и другие соглашения.

Что делает ключевое слово «dynamic» для процедуры? — delphi

Что происходит с процедурой, когда она объявляется с ключевым словом dynamic?

И каков эффект объявления его с помощью ключевого слова static?

    2 1
  • 12 сен 2020 2020-09-12 22:45:10
  • Michael Vincent

1 ответ

На этот вопрос можно ответить, прочитав документацию.

dynamic ключевое слово вводит метод, который может быть переопределен полиморфно. Семантически это взаимозаменяемо с virtual , но реализовано по-другому. Читайте об этом здесь: http://docwiki.embarcadero.com/RADStudio/Seattle/en/Methods#Virtual_and_Dynamic_Methods

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

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

В Delphi для Win32 виртуальные и динамические методы семантически эквивалентны. Тем не менее, они отличаются в реализации диспетчеризации метода-вызова во время выполнения: оптимизируются виртуальные методы для скорости, а динамические методы оптимизируются для размера кода.

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

Статические методы класса похожи на методы класса, поскольку они вызываются в классе, а не в экземпляре. Разница между методами class static и class заключается в том, что методы класса передаются указателем Self который содержит класс, а статические методы класса — нет. Это означает, что методы класса могут быть полиморфными и статические методы класса не могут. Читайте об этом здесь: http://docwiki.embarcadero.com/RADStudio/Seattle/en/Methods#Class_Static_Methods

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

При всем моем уважении я отсылаю вас к этому вопросу: как я могу найти документацию Delphi?

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

2 мая 2013 г.

Эволюция Delphi: современные возможности

Среда Delphi не стоит на месте. Каждый год выходит новая версия Delphi с новыми возможностями. Компании Borland (Inprise) и Embarcadero всегда стремились сохранять в своих продуктах высокий уровень обратной совместимости, поэтому каждая новая версия Delphi способна почти без проблем компилировать старый код. Тем не менее, некоторые существующие возможности могут не существовать в новом мире и окружении или вести себя иначе.

В этой статье я попробую сделать небольшой обзор современных тенденций развития языка Delphi и изменений в нём. В целом, статья будет сконцентрирована на новейших измененияx в архитектуре Delphi, доступные в XE4.

Новый компилятор

Среда разработки состоит из компилятора (переводит исходный текст программы в машинный/виртуальный код), компоновщика/linker (собирает программу из готовых блоков, созданных компилятором), отладчика (debugger), редактора кода (и вообще, в целом — визуальной оболочки) и дополнительных утилит. Ну и, конечно же, среда разработки зависит от языка и библиотек на нём. Всё вместе это называется toolchain (букв. «цепочка утилит») — набор утилит для создания приложений. Слово «цепочка» намекает на то, что результат работы одной утилиты используется следующей (т.е. редактор -> компилятор -> компоновщик -> отладчик).

Среда Delphi является развитием языка Pascal. Toolchain Delphi является закрытой (проприетарной) разработкой Borland. За всю историю Delphi она поддерживала несколько платформ (Win16, Win32, Win64, Linux/CLX, .NET). Под каждую платформу был свой собственный компилятор, который был монолитным. Исходный код компилировался компилятором непосредственно в машинный код целевой платформы (файлы .dcu и .obj).

В этой ситуации добавление новой платформы было непростым делом, поскольку требовалось разрабатывать компилятор для неё с нуля. Дополнительными сложностями был перенос существующего код RTL и VCL, завязанного на конкретную платформу (Win32). Сегодня доля Windows уменьшается, а на сцену выходят более молодые платформы: от Apple и Google. Причём актуальные платформы меняются намного быстрее, чем это происходило в прошлом. В ситуации с таким динамическим изменением имеет смысл упростить разработку компилятора, чтобы более оперативно реагировать на изменения и вносить новые возможности.

Поэтому, центральной идеей ближайшего развития Delphi становится модульный компилятор. Идея заключается в том, чтобы разделить (ранее монолитный) компилятор на две части: т.н. front-end и back-end. Front-end компилятора берёт исходный код программы и переводит его не в машинный код конкретной платформы, а в (универсальный) виртуальный код — т.н. байт-код. Байт-код — это максимально универсальное представление логики программы, не зависящее от языка и платформы. Back-end работает по результату работы front-end: он преобразовывает байт-код уже непосредственно в машинный код конкретной платформы.

Таким образом, вместо того, чтобы делать компилятор полностью для каждой новой платформы, можно оставить front-end неизменным (а ведь именно он отвечает за синтаксис языка), а написать только новый back-end. Более того, вместо того, чтобы использовать собственную проприетарную (и ни с кем не совместимую) разработку, можно использовать широко известное решение (в качестве back-end, конечно же) — получив при этом не только частично готовый код, но и совместимость с некоторыми сторонними утилитами. В качестве такого известного решения разработчики Delphi решили использовать LLVM (Low Level Virtual Machine) — это универсальная система анализа, трансформации и оптимизации программ, реализующая виртуальную машину с RISC-подобными инструкциями.

LLVM используется, в частности, в компаниях Adobe, Apple и Google (например, iPhone SDK использует back-end LLVM). Apple и Google являются одними из основных спонсоров проекта. В настоящее время для LLVM есть back-end-ы для x86-32, x86-64, ARM, PowerPC, SPARC, MIPS, Qualcomm Hexagon и front-end-ы для С, C++, Objective-C, Fortran, Ada, Haskell, Java, Python, Ruby, JavaScript, GLSL (в т.ч. — Clang и GCC). А теперь ещё к front-end добавляется и Delphi. Конечно же, LLVM понятия не имеет про Паскаль и Borland-ский форматы файлов. Но Delphi может иметь свой собственный front-end, который будет компилировать исходный код Паскаль в байт-код LLVM (называемый LLVM IR — «Intermediate Representation», т.е. «промежуточное представление»). А готовый back-end от LLVM может скомпилировать IR от front-end Delphi в машинный код x86-32, x86-64 или ARM. Хотя LLVM IR похож на готовый байт-код для некой виртуальной машины или JIT-компилятора, он всё же нацелен именно на чёткое разграничение front-end и back-end и может рассматриваться как вывод компилятора — аналогично .dcu (Delphi) и .obj (C++ Builder) файлам.

Итак, теперь должно быть очевидным, что в будущем Delphi будет иметь новый компилятор, совместимый с LLVM — и начнётся это уже сейчас, начиная с компилятора для iOS (ARM). А для C++ Builder новая эра началась ещё в прошлом году: 64-битный компилятор C++ Builder сделан уже на новой архитектуре (LLVM). Конечно же, компилятор — это ещё не всё. Нужен ещё компоновщик, отладчик, библиотека поддержки языка (RTL), а для визуального языка — ещё и визуальная библиотека (такая как VCL, CLX, FMX). Также важно отметить, что LLVM в каком-то смысле «подталкивает» разработчиков front-end-ов использовать определённые подходы к управлению памятью, потоками и исключениями. Хотя это и всего лишь «толчок», а не железное ограничение. Стоит отметить, что для мобильных платформ распространена практика использовать LLVM (или виртуальные среды типа Java и .NET), которые поддерживают автоматическое управление памятью: или сборку мусора (garbage collection) или автоматические ссылки (ARC — Automatic Reference Counting). В итоге, вывод: автоматическое управление памятью более предпочтительно, т.к. оно более проработано, поддерживается мобильными устройствами и более привлекательно для новичков.

Итак, сегодня в Delphi (и я говорю про Delphi XE4) есть пять компиляторов: для Win32, Win64, MacOS, эмулятор iOS (компилирует в x86) и iOS (компилирует в ARM). Компиляторы для Win32, Win64, MacOS и эмулятор iOS являются классическими, а компилятор для iOS основан на новой архитектуре LLVM. Как я сказал выше, C++ Builder отличается тем, что компилятор для Win64 у него тоже является новым (LLVM).

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

Изменения в языке

Несмотря на то, что Delphi уже давно поддерживает несколько платформ, до сих пор язык Delphi не претерпел никаких хирургических вмешательств по отсечению старых возможностей. Компилятор для каждой новой платформы создавался полностью Borland/CodeGear/Embarcadero и педантично тащил за собой весь багаж обратной совместимости.

Сейчас ситуация несколько иная. Во-первых, необходимо сделать компилятор (front-end) из Паскаль кода в LLVM IR — что потребует тщательного воспроизведения всего накопленного багажа из обратной совместимости. Во-вторых, ввод нового компилятора совпадает с введением поддержки мобильных платформ. Перенос старого уже написанного кода на мобильную платформу, вероятно, и так потребует пересмотра. В-третьих, добавление новых платформ требует введения в язык новых возможностей. Частично они будут перекрывать старые. В языке будет несколько способов сделать одно и то же. Язык станет слишком сложным сам по себе, не говоря уже о сложностях изучения его для новичков. В четвёртых, уже сегодня в Delphi есть как избыточность (посмотрите, сколько есть в ней типов строк), так и несогласованность (сравните индексацию с 1 для строк, но с 0 — для списков и массивов).

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

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

Итак, современные (и будущие) изменения в языке Delphi заключаются в следующем:

  1. Строки:
    • всего один тип строк
    • индексируются с 0
    • «неизменяемые» строки (immutable strings)
  2. Улучшения классического механизма автоматического подсчёта ссылок:
    • Автоматические ссылки для объектов
    • Слабые (weak) ссылки
  3. Новые классы и процедуры в RTL для кросс-платформенного кода
  4. Отсутствие пакетов и DLL на некоторых платформах
  5. В будущем:
    • with — deprecated
    • object — deprecated
    • указатели — deprecated
    • ассемблер — deprecated

Строки

Больше всего изменений в новых версиях Delphi приходится на строки. Для этого есть несколько причин:

  • Упрощение модели строк (несколько типов строк)
  • Унификация (1-индексация)
  • Оптимизация (требования более слабых мобильных платформ)

Сохранение обратной совместимости со строками из времён Turbo Pascal/Delphi 1 слишком затратно как для разработчиков самой Delphi, так и для разработчиков на Delphi (особенно новичков).

Единый строковый тип

Сегодня в Delphi есть следующие типы строк:

  • Delphi-строки:
    • Родной ( string ) — псевдоним для UnicodeString
    • UnicodeString (счётчик ссылок, Unicode, длина, размер символа, нуль-терминированная)
    • AnsiString (счётчик ссылок, Ansi, длина, размер символа, нуль-терминированная)
    • AnsiString[ кодовая-страница ] (счётчик ссылок, кодовая страница, длина, размер символа, нуль-терминированная)
    • RawByteString (счётчик ссылок, длина, размер символа, нуль-терминированная)
  • Pascal-строки:
    • ShortString (Ansi-кодировка, 255 символов, счётчик длины в первом символе)
    • String[ число ] (Ansi-кодировка, менее 255 символов, счётчик длины в первом символе)
  • C-строки:
    • PChar — псевдоним для PWideChar
    • PAnsiChar (Ansi, нуль-терминированная)
    • PWideChar (Unicode, нуль-терминированная)
  • WideString ( BSTR из COM, Unicode, нуль-терминированная, счётчик длины, специальный API)

Если вы посмотрите на этот список, то заметите следующую вещь: всюду в вашей программе вы оперируете со строками типа string . Все прочие типы строк нужны вам исключительно для совместимости со сторонним кодом: вашим же старым кодом (AnsiString или Pascal-строки), ОС (нуль-терминированные или BSTR ) и т.п. Такой зоопарк не только вызывает путаницу (вопросы вида «в чём разница между WideString и UnicodeString ?»), но и весьма сложен для переноса на другие платформы (чему равен WideString на iOS?). Поэтому идея заключается в том, чтобы оставить один тип строк — самый удобный и универсальный. Гораздо лучше использовать не строковые типы (записи/классы) для коммуникации с внешним миром — так их семантика будет понятнее. А перегрузка операторов сделает безболезненным операции присваивания.

Именно поэтому на новых LLVM компиляторах iOS есть только тип string . Все прочие типы строк там не объявлены и при попытке ими воспользоваться сгенерируют вам ошибку вида «Undeclared identificator AnsiString». Новый тип string в целом равен UnicodeString (т.е. хранит данные строки в UTF-16, имеет счётчик ссылок и длины, а также поле кодовой страницы, которое перманентно равно CP_UTF16 = 1200 ($4B0), и поле размера символа, которое перманентно равно 2 байтам).

Однако сказанное не означает, что вы не сможете работать с данными строк других форматов — просто вы не сможете это делать со встроенными (native) типами данных. Например, предположим, вам нужно использовать текстовые данные в формате UTF-8. Вы можете использовать классы типа TTextReader или TEncoding (которые, кстати, тоже появились в Delphi довольно давно), например:
Этот простой код скрывает от вас всю работу с UTF-8 строками. А вот вариант с явным преобразованием:

Вам может потребоваться хранить строковые данные в других форматах в памяти (например, при вызове сторонних API функций) — в этом случае вам нужно использовать класс TEncoding и хранить строковые данные в (динамическом) массиве байтов ( TBytes ). При желании вы можете даже эмулировать поведение старого компилятора путём введения типов с перегрузкой операторов, например:
Реализация этого класса может использовать TEncoding для работы (конкретно — TUTF8Encoding ). Используя такую запись, вы можете продолжать использовать старый код вида:

0-индексируемые строки

Как известно, первый символ в любой строке Delphi имеет индекс 1, а не 0, как может ожидать любой программист, ранее не знакомый со строками в Delphi. Это называется 1-индексацией (или индексацией с единицы). 1-индексация строк усугубляется тем, что другие структуры в Delphi (динамические массивы, списки и т.п., а также не-Delphi строки) индексируются с нуля (используют 0-индексацию). Получается некоторая путаница и непривычные корректировки на +/-1 в коде по работе со строками.

Историческая справка: почему в Delphi строки индексируются с 1?
Delphi является наследником языка Pascal. В Паскале не использовались 0-терминированные строки из C. Вместо этого Паскаль использовал так называемые «короткие» строки: первый байт строки служил счётчиком символов (= «байтов» в Паскале) в строке. Таким образом, в отличие от строк C строки Паскаля могли хранить #0 внутри строки и очень быстро определять длину (не нужно было искать терминатор в строке, не было цикла), но были ограничены 255 символами (т.е. строка занимала максимум 256 байт вместе со счётчиком).

Соответственно, в Паскале строки технически индексировались с нуля, но нулевой символ отводился под счётчик длины строки, а данные строки начинались с символа №1. Т.е. данные строки индексировались с единицы.

Когда Delphi ввела длинные строки (AnsiString в Delphi 2), то, хотя у длинных строк уже не было счётчика длины в первом символе строки (теперь он хранился в скрытом заголовке строки), индексацию с 1 оставили по соображениям обратной совместимости — чтобы не пришлось переделывать уже написанный код, который работал со строками в предположении, что они индексируются с 1.

Таким образом строки в Delphi стали индексироваться с 1.

Совместно с введением одного единственного строкового типа решено было изменить и этот аспект поведения строк. Поскольку подобное изменение весьма значительно для языка, но не привязано к архитектуре компилятора, то было решено контролировать этот аспект директивой компилятора: $ZEROBASEDSTRINGS . Кстати, эта директива впервые появилась ещё в XE3. По умолчанию эта директива выключена в Delphi XE3, а в Delphi XE4 она выключена для Win32, Win64 и OSX и включена для iOS и эмулятора iOS. Поскольку эта опция контролируется директивой, то вы можете включить её для Delphi XE3 (чтобы начать миграцию раньше). Более того, вы можете выключить её для iOS, чтобы компилировать старый код.

На что нужно обратить внимание:

  • Внутренняя структура строк не меняется. Иными словами не существует такого понятия как «0-индексированная строка». Строка — это строка. Индексация — это лишь способ доступа к данным, он не влияет на сами данные. Т.е. вы можете смешивать в одном проекте модули, собранные с разными настройками. Более того, вы можете иметь разные настройки для разных функций в рамках одного модуля.
  • Все новые функции в Delphi (хэлпер TStringHelper , TStringBuilder ) используют новую семантику (0-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора.
  • Все классические функции RTL ( Copy , Pos , Delete и т.п.) всегда используют прежнюю семантику (1-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора. Тем не менее, Embarcadero рекомендуют не использовать старые RTL-функции (используйте TStringHelper и TStringBuilder ).

Другими словами, опция $ZEROBASEDSTRINGS влияет только на вычисление выражений вида StrVar[ число ] . Посмотрите на такой код:
В любых предыдущих версиях Delphi (XE2 и ниже), а также в XE3 и выше с выключенной опцией $ZEROBASEDSTRINGS вы получите ‘ HOllo foo ‘. Но если вы добавите <$ZEROBASEDSTRINGS ON>перед кодом (либо запустите его на iOS, где эта опция уже включена), то получите ‘ HeOlo foo ‘. Единственная разница между этими двумя кусками — способ вычисления S[2] : в первом случае вы обращаетесь ко второму элементу, который имеет индекс 2 (отсчёт с 1), во втором случае вы обращаетесь к третьему элементу, который имеет индекс 2 (отсчёт с 0).

Примечание: в предварительных обсуждениях релиза XE4 было несколько заблуждений относительно строк. Заметьте, что способ интерпретации выражения в квадратных скобках для строк вообще не зависит от структуры строки, а остаётся на усмотрение компилятора. В самом деле, вы и ранее использовали 1 как индекс для первого символа длинных строк, но как второй символ для коротких строк (первый символ занят под счётчик и имеет индекс 0). Т.е. строки остаются теми же самыми, меняется только способ вычисления компилятором выражения StrVar[ число ] . Вы не передаёте в функцию «0-индексированную строку», вы передаёте «просто строку». Это означает, что вы можете смешивать в одном проекте и модули функции, скомпилированные с разными настройками. Посмотрите на такой код:
По умолчанию, в Delphi XE4 этот код покажет 2/1/1/2 на Windows и 2/2/1/2 на iOS. И снова: единственное отличие — интерпретация выражения в квадратных скобочках. И снова: вы можете изменить поведение на любой платформе на обратное, используя $ZEROBASEDSTRINGS .

Если вы хотите написать универсальный код, который будет работать для обоих вариантов $ZEROBASEDSTRINGS , то вы можете определить константы, зависящие от значения Low(string) , которое будет равно 1 и 0 для <$ZEROBASEDSTRINGS OFF>и <$ZEROBASEDSTRINGS ON>, соответственно. Например: Этот код будет работать всегда одинаково, вне зависимости от настроек компилятора. А вот как вы можете работать с циклами: Low(S) возвращает 0 для 0-индексированной строки и 1 — для 1-индексированной. High(s) возвращает Length(S) — 1 для 0-индексированной строки и Length(S) — для 1-индексированной. В случае пустой строки Low , конечно же, возвращает всё то же значение, а High возвращает -1 или 0, соответственно. Вы можете передать тип вместо переменной в Low , но это не сработает для High .

Вместо Low и High вы можете использовать хэлпер для строк, который появился в Delphi XE3. Фактически, в Delphi XE3 появилась новая возможность: возможность добавлять методы любым встроенным типам данным, а не только записям и классам. Хотя синтаксис несколько необычен для Delphi: Кроме самой возможности в Delphi XE3 были введены и некоторые новые конструкции, использующие новую возможность. Среди них: TStringHelper — хэлпер для типа string . Он объявлен в модуле SysUtils и предоставляет методы вида Compare , Copy , IndexOf , Substring , Length , Insert , Join , Replace , Split и многие другие. Поэтому теперь вы можете написать: Заметьте, что все эти методы (включая индексированное свойство Chars ) используют индексацию с нуля вне зависимости от настроек компилятора.

Immutable-строки

Несмотря на то, что новый единый тип string по-прежнему эквивалентен бывшему UnicodeString , внутренняя реализация строк может быть изменена в будущем и/или на других мобильных платформах. Уже сейчас предполагается, что строки станут неизменяемыми (т.н. immutable-строки): это означает, что строку нельзя изменить когда она была создана. Этот аспект не влияет на операции типа конкатенации (сложения строк), потому что эти операции создают новую строку из каких-то других строк. Immutable-строки влияют на in-place операции вида S[1] := ‘A’; — такие операции «запрещены».

Ещё раз: сегодня строки по прежнему изменяемы в любых компиляторах (в том числе — для iOS). Конструкции вида S[1] := ‘A’; полностью разрешены (в том числе — для iOS). Тем не менее, в будущем этот аспект может быть ограничен.

Сегодня все компиляторы Delphi используют семантику copy-on-write (копирование-при-записи): если вы модифицируете строку, а она имеет счётчик ссылок больший 1, то строка копируется в новую, и изменения вносятся в копию, оставляя старую версию неизменной — так что все остальные (кто держит ссылку на строку) не увидят вашего изменения. Иными словами, вместо копирования строки изначально при присваивании, техника copy-on-write копирует строку позже — когда её необходимо изменить. Копирования может и не произойти, если вы не модифицируете строку. Внутренне это достигается (скрытыми) вызовами UniqueString для строк вида S[1] := ‘A’; . Разумеется, вам нужно вставлять вызовы UniqueString вручную, если вы работаете с содержимым строки напрямую (через указатели).

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

Уже сегодня вы можете найти потенциальные пробные места в вашем коде. Для этого вы можете включить подсказки компилятора директивой <$WARN IMMUTABLE_STRINGS ON>. С включенной опцией компилятор будет выдывать такие предупреждения:

[dcc32 Warning]: W1068 Modifying strings in place may not be supported in the future”

И снова: если вы используете в вашем коде только конкатенацию строк, то immutable-строки вас не коснутся — ни сейчас, ни в будущем (в самом деле, оптимизация конкатенации — это одна из целей для развития Delphi). Разработчики Delphi не ожидают, что конкатенация строк станет медленнее или будет запрещена в будущем. Только изменение индивидуальных символов (содержимого строки) может вызвать проблемы в будущем (а может и не вызвать).

Тем не менее, сегодня операция конкатенации может быть не самым оптимальным способом работы со строками на мобильной платформе. Вы можете знать, что в Delphi уже давно есть специализированный класс для построения строк: TStringBuilder . Несмотря на то, что этот класс присутствует в Delphi уже давно (начиная с Delphi 2009), он не пользуется популярностью. Почему? Посмотрите на такой код:
На Desktop-платформах подобный код даст следующие результаты:
Иными словами, на мощных платформах нет никакой выгоды от использования TStringBuilder , поскольку умный менеджер памяти (типа FastMM или даже встроенного в ОС) успешно выполняет ту же работу, что и TStringBuilder (работу по динамическому росту блоков памяти).

Но на мобильных платформах ситуация иная: более слабые платформы не имеют такого же сложного и умного менеджера памяти, как и Desktop-ы. Поэтому результат работы такого кода на iOS будет следующим:
В этом случае TStringBuilder почти в два раза быстрее простого сложения строк.

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

Улучшения классического механизма автоматического подсчёта ссылок

Delphi для iOS вводит в язык поддержку ARC (Automatic Reference Counting) — «автоматический подсчёт ссылок». ARC является улучшенным механизмом подсчёта ссылок, который существовал в Delphi со времён Delphi 2 — для строк, вариантов, динамических массивов и интерфейсов. Фактически, единственными данными, управляемыми вручную, в Delphi являлись объекты и указатели. И если указатели уже давно успешно вытесняются управляемыми аналогами, то объекты продолжали оставаться типами с ручным управлением, плодя бесконечные вложенные иерархии try-finally в вашем коде.

До сегодняшнего дня. Сегодня ARM компилятор Delphi вносит автоматическое управление временем жизни и в объекты.

Автоматические ссылки для объектов

ARC является механизмом автоматического учёта памяти. Часто ему противопоставляют реализацию автоматического учёта памяти из .NET, называемую (несколько ошибочно) сборкой мусора (garbage collection). Оба механизма служат одной цели, но делают это разными способами. Напомню, что менеджер памяти .NET периодически запускает подпрограмму очистки памяти, которая пытается найти блоки памяти (или группы блоков), на которые нет внешних ссылок. Здесь же видно, в чём отличие двух подходов: ARC 100% детерминирован — память освобождается всегда в один и тот же момент (когда счётчик ссылок падает до нуля), способ .NET может освобождать память позднее, чем она реально отпускается. Кроме того, освобождение памяти (и, следовательно, объектов) в ARC выполняется текущим же потоком, а не фоновым потоком-уборщиком, как это происходит в .NET. Однако, ARC всё ещё допускает возможность утечек памяти, если вы создадите циклическую ссылку (первый объект указывает на второй, а второй — на первый), в то время как .NET увидит два блока памяти, изолированные от остальных, и удалит их.

Примечание: хотя ARC реализован только в (LLVM) компиляторе для iOS, его эмуляция также доступна на (классическом) компиляторе «эмулятор iOS». ARC не доступен в компиляторах для Win32, Win64 и OSX.

Использовать ARC очень просто — вам практически не нужно думать об управлении памятью. В вашей практике вы постоянно использовали строки ( string ) и практически никогда не задумывались об управлении памяти для них. Точно так же вы теперь можете поступать и с объектами:
Ближайший аналог ARC для объектов — это работа с интерфейсами (interface) в Delphi. Если вы когда-либо работали с интерфейсами в Delphi, то теперь точно так же сможете работать и с обычными объектами.

Точно так же, как с интерфейсами (и любыми другими типами с автоматическим управлением памятью в Delphi), вы можете удалить ссылку преждевременно (до выхода переменной за область видимости) путём присвоения переменной значения nil :
Хотя строка » end » по прежнему будет содержать (скрытый) блок finally с очисткой MyObj — в этом варианте кода «магия» компилятора отработает вхолостую, поскольку вы сами освободили ссылку до выхода из подпрограммы. Разумеется, если метод DoSomething вызовет исключение, то строка с присвоением nil будет пропущена, и тогда объект, как и ранее, будет удалён из «подстилки» компилятора в строке » end «.

Заметьте, что в этих примерах отсутствуют явные блоки try-finally — и код при этом остаётся 100% корректным. Это благодаря тому, что блок try-finally теперь является скрытым. Теперь вам не нужно писать многоуровневые вложенные блоки try-finally ! Фактически, то, что делает сейчас ARC, эквивалентно такому коду (который, впрочем, вы и сами могли писать ранее вручную):

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

  • Использовать <$IFDEF AUTOREFCOUNT>, разделив код на два варианта.
  • Использовать классический подход с Free / FreeAndNil , не используя преимущества ARC. На ARC этот подход формально будет работать благодаря обратной совместимости, хотя его поведение может незначительно отличаться.

По первому пункту: новый компилятор предоставляет следующие (новые) символы условной компиляции (определения для компиляторов даны по состоянию на XE4):

Символ: Условие: Компиляторы:
NEXTGEN Новый компилятор dcciosarm, dccios32
AUTOREFCOUNT Доступен ARC dcciosarm, dccios32
CPUARM Для процессоров с архитектурой ARM dcciosarm
IOS Целевая платформа — iOS dcciosarm, dccios32
WEAKREF Компилятор может использовать слабые ссылки dcciosarm, dccios32

Примечание: все символы условной компиляции можно посмотреть здесь.

По второму пункту: разумеется, разработчики Delphi не могли просто «выбросить на свалку» базилионы написанных сторонними разработчиками строк кода на Delphi, объявив их «устаревшими» и «несовместимыми с новой моделью». К примеру, если рассмотреть такой классический код:
В классическом компиляторе, где объекты являются неуправляемыми типами данных, вызовы FreeAndNil , Destroy или Free безусловно удаляли существующий объект. В новых компиляторах с поддержкой ARC этот код будет работать немного иначе: вызовы FreeAndNil , Destroy и Free будут эквивалентны » := nil » (т.е. очистке ссылки). Иными словами, блок кода выше в компиляторе с ARC будет скомпилирован как:
Что является 100% рабочим и корректным кодом, пусть и не самым разумным и эффективным. Иными словами, старые вызовы FreeAndNil / Free / Destroy полностью допустимы и безопасны, хотя и бесполезны в компиляторах с ARC.

Однако это не означает, что вы сможете использовать весь свой старый код без модификаций. В старом коде у вас могут быть более сложные ситуации — например, несколько ссылок на один объект. С классическим компилятором висячая ссылка (вы удалили объект по одной ссылке, но остальные ссылки не были сброшены) ваш объект удаляется, но на него продолжают указывать ссылки. Это — допустимо, если вы не обращаетесь к объекту по висячим ссылкам. Но в новой модели эти висячие ссылки добавят «+1» к счётчику ссылок объекта. Таким образом, очистка ссылки вызовом FreeAndNil / Free / Destroy уменьшит счётчик, но не до 0. Т.е. объект удалён не будет. Само собой, это не означает утечки памяти — объект всё же будет удалён, но позже — когда удалится последняя (ранее «висячая») ссылка. Так что ваш код может работать и как ранее (только изменится картина выделения/освобождения памяти), но, быть может, вам необходимо очистить объект до наступления другого события (такого, как выгрузка библиотеки, из которой объект и получен). В этом случае ваш код может вылететь. Решение заключается в правиле, которому не грех было бы следовать и ранее (ещё с классическим компилятором): не оставляйте висячих ссылок. Очищайте все ссылки на объект при его удалении.

Альтернативным решением задачи гарантированного вызова деструктора может быть вызов (нового) метода DisposeOf :
Метод DisposeOf безусловно вызывает деструктор — даже несмотря на существующие ссылки на объект. После такого вызова деструктора объект переходит в состояние «зомби» («zombie state» или «disposed state») — для него был вызван деструктор, объект был очищен, но память для него ещё не была освобождена. Вы можете узнать состояние объекта через свойство Disposed — это аналог Assigned для объектов из классического компилятора.

Разница между вызовами FreeAndNil / Free и DisposeOf заключается в ваших намерениях: вызов FreeAndNil / Free отсоединяет ссылку, но не означает немедленного удаления объекта (он может быть удалён сейчас, но может быть удалён и позднее), а вызов DisposeOf всегда безусловно удаляет объект, даже если на него есть ссылки.

Примечание: «зомби»-объект никак не защищается от возможного ошибочного доступа к нему. Вы можете прочитать/записать свойство, вызывать методы (как обычные, так и виртуальные) — все эти операции будут успешными, но будут оперировать на уже очищенном объекте. И хотя это не приведёт к Access Violation, как в классическом компиляторе с висячими ссылками (потому что память под «зомби» объект всегда гарантировано выделена), но все структуры данных объекта уже были очищены деструктором, что может привести к неожиданному поведению. Всегда проверяйте статус объекта вызовом Disposed , если вы удаляете объект вручную. Кроме того, вы можете проверить доступность объекта в самих методах объекта вызовом protected -метода CheckDisposed — это некий аналог Assert(Disposed); .

Заметьте, что старый Assigned вместе с FreeAndNil больше не имеют смысла в новой архитектуре, потому что объект всегда гарантировано существует (пусть даже и как зомби), пока на него есть хоть одна ссылка — это отличается от классической модели, где вам приходилось записывать в ссылку nil , чтобы указать на уже удалённый объект. (Хотя, конечно, вы можете продолжать использовать Assigned , если вы очищаете ссылки на объекты до их выхода из области видимости.)

К счастью, вам не нужно увлекаться <$IFDEF AUTOREFCOUNT>, потому что и DisposeOf и Disposed доступны и в классических компиляторах (начиная с XE4, конечно же). Код выше будет полностью работоспособен и в Win32, где вызов DisposeOf просто вызывает Free , ну а Disposed всегда возвращает False , поскольку в классическом компиляторе нет состояния «зомби». Поэтому, если у вас есть старый код и вы хотите точно такого же поведения (т.е. удалять объект сразу, а не когда уйдёт последняя висячая ссылка на него), то вы можете просто заменить вызовы FreeAndNil / Free / Destroy на вызов DisposeOf . К несчастью, вместо двух состояний «есть объект»/»нет объекта» у вас теперь появляется три состояния: «есть объект»/»зомби»/»нет объекта» — что, впрочем, не сильно отличается от бывшего «есть объект»/»висячая ссылка — непонятно, есть объект или нет»/»нет объекта» — которое в классическом компиляторе вы должны были сводить к «есть объект»/»нет объекта». В связи с этим, вам может пригодится такая подпрограмма:
Эту функцию можно использовать во всех местах, где вы раньше использовали if Assigned(Obj) then — замените их на if ValidObject(Obj) then .

Примечание: деструктор в ARC по прежнему называется Destroy , но он заблокирован для прямого вызова (помещением в секцию protected ). Поэтому:

  1. Добавьте <$IFDEF AUTOREFCOUNT>protected <$ENDIF>перед каждым destructor Destroy; override;
  2. Замените все внешние вызовы Destroy (если они вдруг у вас есть) на FreeAndNil / Free или DisposeOf — смотря по тому, согласны ли вы с отложенным удалением объекта или вам нужно немедленное удаление.

Суммируя сказанное, вот современная реализация TObject (показан только код, имеющий отношение к циклу создание-удаления объектов):

Слабые (weak) ссылки

Однако поддержка ARC в Delphi касается не только расширением действия счётчиков ссылок на классы/объекты, но и поддержки слабых (weak) ссылок. Слабые ссылки предназначены для решения проблемы циклических ссылок. Наиболее типичный случай возникновения циклических ссылок: контейнер-коллекция, в котором его элементы содержат ссылки на него самого (как на контейнер-владелец). В классической модели ссылок из Delphi подобная конструкция порождает утечку из-за наличия циклической ссылки.

Здесь на сцену выходят слабые ссылки. Слабая ссылка — это ссылка на объект, которая не приводит к изменению счётчика ссылок. Иными словами, при присвоении объекта в переменную со слабой ссылкой не происходит увеличение счётчика ссылок объекта на единицу. Аналогично, при очистке слабой ссылки не происходит уменьшение счётчика объекта на единицу. Создать слабую ссылку очень просто — достаточно пометить переменную атрибутом [weak] , например:
В этом примере поле FOwnedBy является слабой ссылкой, потому что оно помечено атрибутом [weak] . Это означает, что присвоение этому полю не увеличивает счётчик ссылок присваевомого объекта, а его очистка — не уменьшает счётчик ссылок объекта. Таким образом, создание экземпляра TMyComplexClass не приведёт к утечке памяти, несмотря на наличие циклической ссылки — благодаря тому, что одна из ссылок в составе циклической ссылки является слабой.

Вы можете увидеть, что атрибут [weak] используется и в коде самой Delphi, например:

Примечание: вы можете использовать атрибут [weak] и в классических компиляторах, но он будет игнорироваться, поскольку в этих компиляторах нет ARC. Таким образом, если вы пишете универсальный исходный код — вам необходимо как помечать переменные атрибутом [weak] , так и использовать FreeAndNil / Free (использование которых допускается в компиляторах с ARC).

Вы также не можете проверить статус объекта по слабой ссылке. Чтобы проверить статус объекта, вам сначала нужно присвоить объект в обычную переменную, например:

Диагностика с ARC

Использование ARC упрощает работу с памятью и снижает риск утечек памяти/ресурсов в вашем коде, но поскольку всё же существует вероятность создать циклическую ссылку, то ваш код всё ещё не полностью защищён от утечек памяти.

С целью отладки вы можете использовать свойство RefCount , чтобы узнать число живых ссылок на объект. Не следует использовать это свойство для реализации логики программы. Кроме того, вы можете (крайне редко) использовать __ObjAddRef и __ObjRelease для ручного управления счётчиком ссылок — например, для записи объекта в неуправляемую переменную-указатель (к примеру, свойства типа Tag / Data ). Этот приём допустимо использовать в логике кода, хотя его и нужно избегать (предпочтительнее: создание наследника с полем нужного типа).

Вы можете проверить наличие циклических ссылок с помощью функции CheckForCycles (модуль Classes ):
Как и выше, эта функция служит для диагностики, её не следует использовать в финальной сборке программы/для реализации логики самой программы. Используйте её только для отладки.

Заметьте, что по аналогии со строками и интерфейсами ARC с объектами является потокобезопасным: при работе со счётчиком ссылок используются атомарные interlocked-операции. Заметьте, что это не означает автоматической потокобезопасности самих объектов.

Обратите внимание, что на мобильных платформах Delphi использует функции операционной системы в качестве штатного менеджера памяти. Иными словами, FastMM — штука чрезмерно сложна для мобильной платформы и, более того, написанная на x86-ассемблере (иными словами: непереносимая). Поэтому возможности Delphi по диагностике утечек памяти будут недоступны. Вы можете использовать инструментарий целевой ОС или использовать сторонние фильтры.

Новые классы и процедуры в RTL для кросс-платформенного кода

В целом вы должны избегать прямых платформенных вызовов (т.е. функций Windows/Mac/iOS API) и использовать, предлагаемые Embarcadero обёртки-переходники. Конечно же, вы также должны как чумы избегать ассемблера и, желательно, не использовать указатели.

Например, Embarcadero предлагает вам модуль IOUtils. Он доступен, начиная с Delphi 2010. Вы можете прочитать про него здесь. Как можно догадаться, этот модуль предоставляет вам кросс-платформенные возможности для работы с файлами. В нём есть классы TDirectory , TPath и TFile — для работы с каталогами, именами файлов и файлами соответственно.

К примеру, вы можете получить доступ к папке «Documents» на мобильном устройстве так же, как вы получаете доступ к папке Application Data в Windows:

А вот как вы можете искать файлы: этот код считывает подпапки заданной папки, а затем считывает файлы в найденных подпапках:

Библиотеки и пакеты

К сожалению, одна из древнейших возможностей Delphi — использование пакетов времени выполнения (run-time packages, BPL) и, более обще, DLL — не поддерживается на платформе iOS. Пакеты и библиотеки представлены DLL на Windows, dylib на MacOS и so (shared object) на Linux. Они позволяют вам создавать модульные приложения. Но на iOS приложение не может устанавливать библиотеки — это может делать только сама Apple, а iOS приложения обязаны быть монолитными программами.

Тем не менее, компилятор Delphi умеет распознавать статические ссылки на DLL (например, на midas.dll) и внедрять их в приложение статически, а не как отдельные библиотеки.

«Плохие» конструкции

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

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

Object (старые объекты Паскаля) устарели много лет назад. Замените их на записи (record).

Ассемблерный код уже подвергся ограничению при переходе на Win64 (нельзя внедрять ассемблерный код в середину функции), а в будущем он может быть ограничен ещё больше. Уже сейчас он не поддерживается компилятором iOS. Ассемблерный код привязан к конкретной платформе и не портируется на другую платформу. Постарайтесь не использовать его.

Прямой доступ к указателям есть не на всех платформах. Хотя даже сегодня использование указателей не поощряется (как подверженное ошибкам), но они всё ещё полностью поддерживаются всеми компиляторами Delphi. Только надо иметь в виду, что в будущем их использование может быть ограничено или вовсе отсутствовать для некоторых платформ. Уже сейчас указатели удаляются из языка в пользу ARC (к примеру, в Delphi для iOS отсутствует модуль System.Contnrs , поскольку он основан на TList с указателями). Поэтому если у вас есть выбор, использовать указатели или безопасный аналог — не используйте указатели.

К примеру, TList и TStringList являются своеобразными «швейцарскими ножами»: они используются как универсальный контейнер на все случаи жизни, благодаря способности хранить произвольные ссылки (для TStringList — через свойство Objects ). Но новые версии Delphi поддерживают дженерики (generics) и имеют более узкоспециализированные классы — и их использование будет предпочтительнее по двум причинам: меньше ошибок (нет приведений типов) и быстрее выполнение (может использоваться хэш-таблица).

Рассмотрим такой код с двумя идентичными списками:
Списки заполняются случайными (но идентичными для обоих списков) значениями в цикле:
Попробуем получить каждый объект в обоих списках по его имени (ключу). Оба списка содержат идентичный набор данных, а имена ключей (объектов) хранятся в отельном списке ( sList ):
Сколько времени займёт поиск в отсортированном списке строк (который использует двоичный поиск в случае отсортированного списка) по сравнению со словарём (который использует хэш-ключи)?
Результат работы обоих вариантов кода идентичен (предполагая, что на вход поступил один и тот же набор данных), но скорость выполнения значительно отличается: TStringList работает в четыре раза медленнее словаря (пример дан для миллиона записей).

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

Заключение

С выходом первого компилятора Delphi для ARM, основанного на архитектуре LLVM, язык Delphi переживает важный переходный период. Хотя разработчиками Delphi были предприняты усилия для поддержания обратной совместимости, разработчикам рекомендуется использовать новые возможности языка и двигаться вперед.

Описанные в этой статье возможности языка (в частности — поддержка ARC) будут формировать будущее Delphi. Эти изменения частично обусловлены поддержкой новой платформы, а частично предназначены для исправления некоторых «плохих» мест в языке Delphi.

Суммирующая табличка по компиляторам для Delphi XE4 (компиляторы для C++ Builder не показаны, за исключением Win64):

Условная компиляция в Delphi

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

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

Для начала, рассмотрим очень простой код, который вы можете повторить в Delphi за несколько секунд:

Теперь нажмите F9 и проверьте, что написано в отладчике в «Events»:

Разберемся с тем, что мы только что написали.

$IFDEF — это директива компилятора;

DEBUG — условное определение или символ условной компиляции. Символ обязательно должен начинаться с буквы за которой может следовать любое количество букв, цифр и знаков подчеркивания, однако использоваться будут лишь первые 255 символов.

Процедура отправляет строку в отладчик для отображения.

Завершает условную компиляцию, инициированную последней директивой <$IFxxx>(почему не <$IFDEF>— смотрим далее).

Таким образом, используя директивы и мы указали компилятору дословно следующее: если где-либо по ходу компиляции был встречено условное определение DEBUG, то надо выполнить OutputDebugString.

Где определено условное определение DEBUG? Конкретно в этом случае, символ DEBUG можно найти, если зайти в настройки проекта: Project -> Options ->Delphi Compiler :

Здесь же можно определить и свои собственные символы. Давайте, например, добавим свой символ условной компиляции TEST. Для этого открываем диалоговое окно редактирования символов условной компиляции (жмем кнопку «…» в строке «Conditional defines») и заносим наш символ в список:

Теперь изменим наш код, приведенный в начале статьи, чтобы он работал с нашим символом условной компиляции:

Теперь можете снова запустить приложения в режиме отладки и посмотреть, что в Events появится строка «TEST IS ON».

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

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

и убедиться, что символ DEBUG выключен, а в окне Events не появится строка «debug is on».

Двигаемся далее. Что делать, если нам необходимо вывести строку не когда символ включен, а именно тогда, когда он выключен? Здесь, опять же, есть варианты. Короткий вариант — воспользоваться директивой противоположной — она называется и код между и выполняется, если символ выключен:

Второй вариант — использование директивы , если в зависимости от состояния символа условной компиляции вам надо выполнять различные участки кода:

Соответственно, нет необходимости далее повторять этот же участок кода с использованием — работать будет, но прямо противоположно.

Также следует обратить внимание на то, что все условные символы оцениваются в Delphi, когда вы выполняете Build проекта. Справка Delphi рекомендует для надежности пользоваться командой Project -> Build All Projects, чтобы быть уверенным, что все символы условной компиляции определены верно.

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

Например, символ условной компиляции VER330 определен для Delphi 10.3 Rio и с его помощью можно определить какой код должен или не должен выполняться, в случае, если версия компилятора Delphi — 33. Например, воспользуемся фичей Delphi 10.3 Rio под названием Inline Variable Declaration:

Сразу может возникнуть вопрос: как сделать так, чтобы приведенный выше код сработал не только в Delphi 10.3 Rio, но и в последующих версиях?
Это можно сделать воспользовавшись, например, такой конструкцией:

Здесь мы уже воспользовались директивой с помощью которой проверили значение константы CompilerVersion, которая находится в модуле System.

Здесь же стоит обратить внимание и на окончание блока — мы использовали директиву , как того требовала Delphi до версии Delphi XE4:

  • для директивы $IFDEF должна быть определена директива $ENDIF
  • для директивы $IF должна быть определена директива $IFEND

В XE4 нам разрешили использовать для закрытия блоков <$IF>, и . Однако, если у вас возникают проблемы при использовании связки и , то вы можете использовать специальную директиву , чтобы потребовать использовать для именно <$IFEND>:

Теперь, если в коде выше использовать директиву $ENDIF, то получим сообщение об ошибке:

Директиву , кстати, можно использовать и с другими константами проекта, например, так:

Так как наша константа Version содержит значение 2, то выполнится участок кода расположенный после . Можете сменить значение константы Version на 1, чтобы убедиться, что выполнится участок кода, где определена переменная s.

Как я сказал выше, в Delphi есть целый набор предопределенных символов условной компиляции. Ознакомиться со всеми этими символами вы можете на этой странице справки по Delphi.

  1. Использование условной компиляции позволяет нам выполнять тот или иной код, в зависимости от того, какие константы и символы условной компиляции определены или не определены в проекте.
  2. Используя предопредленные символы условной компиляции можно указывать Delphi какой код необходимо выполнить, например, если программа собирается под Android, или, если поддерживается архитектура x64 и т.д.
  3. Директива $IF может использоваться с различными константами, в том числе и определенными самим разработчиком.

При подготовке статьи использовалась следующая информация официальной справки по Delphi:

Илон Маск рекомендует:  Шаблон для сайта бизнес тематики HTML, CSS, JS
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL