Category: Delphi
Тернарный условный оператор в Delphi/Pascal через функцию IfThen
Очень часто в коде мы пишем вот обычные конструкции if-then-else
внутри которых присваиваем переменной значение.
Для примера напишем простую функцию определяющей минимальное число:
function Min(A, B: Integer): Integer; var Min: Integer; begin if A < B then Min := A else Min := B; Result := Min; end;
А ведь очень часто таких вот конструкций в коде становится много, и они отнимают очень много места (аж четыре строчки!) и внимания у нас при том что они очень просты. Некоторые программисты записывают таки условия в одну строчку:
function Min(A, B: Integer): Integer; var Min: Integer; begin if A < B then Min := A else Min := B; Result := Min; end;
Но такая запись нарушает стандарт стилевого оформления кода и является очень нежелательной.
При этом имя переменной в которую мы записываем значение всё равно дублируется, это мелочь, но неприятно.
Так вот, в старом, добром, вечном, угрюмом и бесчеловечном Си для таких конструкций ввели специальный синтаксис: тернарный условный оператор.
Имеет он такую запись:
переменная = булево_выражение ? значение_переменной_если_выражение истинно : значение_если_ложь;
Вот так можно переписать нашу функцию на Си с его использованием:
int min(int a, int b) { int min; min = (a < b) ? a : b; return min; }
Тернарный условный оператор хорош не только тем что он компактнее, но и тем, что он более точно передаёт смысл кода, улучшая его читабельность.
Впервые он появился ещё в Алголе — праязыке всех современных языков, который успешно ввёл понятие структурного программирования. Паскаль это вообщем то и есть упрощённый Алгол, но вот тернарный оператор он почему то не унаследовал 😦
Зато практически все другие современные языки программирования, которые унаследовались от Си, сохранили синтаксис этого оператора нетронутым, а это: C++, JavaScript (ECMAScript), Objective-C, C#, D, Java, PHP, Perl.
В языках программирования, где такой радости как тернарный условный оператор нет, есть стандартные функции для его имитации.
В Delphi это функция IfThen, о которой как я заметил многие дельфисты не знают.
// Объявленные в стандартном юните Math function IfThen(ACondition: Boolean; ATrue: Integer; AFalse: Integer = 0): Integer; function IfThen(ACondition: Boolean; ATrue: Double; AFalse: Double = 0.0): Double; // Объявленные в стандартном юните StrUtils function IfThen(ACondition: Boolean; ATrue: string; AFalse: string = ''): string;
Её описание из хелпа:
Conditionally returns one of two specified values.
IfThen
checks the expression passed as ACondition
and returns ATrue
if it evaluates to true, or AFalse
if it evaluates to false. If the AFalse
parameter is omitted, IfThen
returns 0
or an empty string when ACondition
evaluates to False
.
Теперь можно переписать наш пример нахождения минимального числа так:
function Min(A, B: Integer): Integer; var Min: Integer; begin Min := IfThen(A < B, A, B); Result := Min; end;
Но есть пару нюансов:
- типами аргументов у нас могут быть только примитивные
Integer
,Int64
,Double
иstring
. С объектами или перечисляемыми типы облом 😦 - Значения аргументов функции вычисляются перед её вызовом, поэтому в отличии от стандартного условного оператора
if-then-else
у произойдёт вычисление всех его веток и это может вызвать ошибку.
Приведу пример, пользователь задал в полеEdit1
высоту формыForm1
. Мы берём строковое значение поляEdit1
, преобразовываем его вInteger
и задаём в качестве высоты формы. Если полеEdit1
пустое задаём форме стандартную высоту в400
пикселей.Вот реализация через
IfThen
:var FormHeight: Integer; begin FormHeight := IfThen(Edit1.Text '', StrToInt(Edit1.Text), 400); Form1.Height := FormHeight; end;
Во время выполнения если мы оставим
Edit1
пустым мы неожиданно получим исключение «Ошибка: пустая строка не есть число». Всё это горе нам потому, что программа сперва пытается вычислить значения передаваемых аргументов, т.е. преобразовать пустую строку в числоStrToIn('')
, а только потом вызываетIfThen
. Это никак не объехать, так что прийдётся написать по старинке через блокif-then-else
.
Кроме знаний этих нюансов, вам так же стоит запомнить два простых правила улучшения читаемости вашего кода:
- Если вычисления в аргументах
IfThen
довольно сложные или записьIfThеn
не вмещается на одну строчку, перепишите этот код через стандартный блокif-then-else
. - Никогда, никогда не делайте вложенные вызовы
IfThen
!Так писать нельзя!N := IfThen(A < 0, -1, IfThen(A = 0, 0, 1));
Потом не разгребётесь.
Используйте IfThen почаще, но с умом и тогда ваш код станет лучше!
[Delphi] Обработчики событий форм в ООП стиле
Как известно, один класс может иметь несколько экземпляров. Но формы в Delphi обычно делаются непригодными для существования нескольких их экземпляров и практически невозможно делать их классы наследники. Особенно актуальной эта проблема становится в MDI приложениях, хотя те же проблемы встречаются практически повсеместно в программах на делфи.
Обработчик событий всегда один
Delphi крут тем, что создание формы делается несколькими щелчками мышки. Реакция на действия пользователей, вроде нажатия на кнопку, тоже делается очень легко и быстро через обработчики событий.
Но в этом его и недостаток, потому что нельзя поместить на одно событие несколько обработчиков. Это всё потому что они являются тупо указателями на методы, а не полноценной реализацией шаблона проектирования Наблюдатель (англ. Observer), как это реализовано в интеллигентных языках программирования, вроде Java.
И такая беда повсюду в делфи, вы и сами наверняка сталкивались с этой проблемой. Вообще использование некоего подобия шаблона Наблюдатель в Delphi я встречал ровно один раз — это хорошо всем знакомый компонент TDataSource
, который следит за компонентами TDataSet
. Observer вообще трудновато реализовать на делфи, об этом я отдельно напишу.
Итак первая проблема: невозможность одновременного использования нескольких обработчиков события, и это архитектурный баг всего Delphi. Но это ещё не всё.
Используйте переопределение методов вместо обработчиков событий
На самом деле, обычно, пока все обработчики событий у вас находятся внутри класса формы это особо большого неудобства не доставляет. Но такие же обработчики у вас могут использоваться для событий самой формы. Классическим примером служат события формы OnCreate
, OnShow
, OnActivate
, OnPaint
, OnClose
. Несмотря на то что они выполняют внутреннюю логику формы, это публичные события и на них могут быть внешние обработчики.
Например форма редактора может вызывать форму настроек шрифта и по её закрытию применять новый шрифт. Т.е. нам нужно два обработчика события закрытия формы: один внутренний, который например сохраняет настройки в ini файл, и один внешний. Учитывая первую проблему мы этого сделать уже не можем.
А всё потому что нарушен контракт: класс должен выполнять обработку своих же событий не через регистрацию обработчиков на них, а внесением логики в защищённые (т.е. protected
) методы а-ля DoSomthing
. Методы должны быть protected для того чтобы можно было переопределить их в наследниках.
И вместо
procedure TForm.Form1OnClose(var Action: TCloseAction); begin Action := caFree; end;
нужно переопределять метод DoClose
унаследованный от TCustomForm
procedure DoClose(var Action: TCloseAction); override; begin Action := caFree; inherited; // Не забывайте дёргать унаследованный метод! end;
И мы сможем освободить место для внешнего обработчика событий! И даже в классах наследниках сможем переопределять логику.
Беда в том что определить какой метод нужно переопределять зачастую геморно, а то и вовсе невозможно. Например для события OnShow
мы можем переопределять либо Show
либо DoShow
. А вот на создание, здравый смысл подсказывает переопределять конструктор, но после создания объекта формы в нём ещё нет ни одного компонента, потому что они ещё должны быть загружены из dfm файла. В итоге мы имеем аж три варианта Create(AOwner: TComponent)
, DoCreate
(предпочтительней) и AfterConstruction
унаследованный ещё от TObject
. А событие OnHelp
вообще никак, ибо вызывается сразу классом TApplication
(ну и не надо).
Я накидал небольшую табличку замен событий формы на переопределённые методы класса формы.
У формы могут быть много экземпляров, удаляйте переменные формы созданные компилятором
Формы в делфи, это практически Одиночки. При создании формы делфи генерирует новый юнит, внутри которого объявляется класс формы и сразу же переменная для этой формы. Также он записывает создание формы в файл проекта.
Сами компоненты сохраняются не в виде кода их создания, как это делается в Java или С#, а в отдельном файле *.dfm, что тоже очень хорошо. Джависты смотрят на делфи с завистью, хотя у них тоже есть инструменты отдельно описывать структуру форм, но они не стандартны и не очень популярны.
Созданное мастером форм делфи приложение будет создавать в памяти все формы, и это лишняя нагрузка. Зачем нам создавать форму настройки шрифта, если пользователь её не открывает? Поэтому обычно делают динамическое создание формы, как это делать написано в каждом учебнике. Но в них не написано, что чтобы достигнуть полной нирваны нужно ещё и удалить переменные формы из юнитов где они объявлены. Ведь форм может быть много а переменная одна, и та не используется. А если кто её заиспользует по невнимательности, то можно отхватить гейзенбаг. В моей практике было несколько таких случаев. Как это ни странно, dfm файлы всё равно корректно работают в этом случае, несмотря на то что в них описан экземпляр формы, а не свойства всего её класса.
PS
Как вы видите все эти проблемы свойственны не только формам. Но раз уж зашла о них речь то я хотел бы отметить другие популярные ошибки и дать пару советов:
- Если у вас есть код который должен выполнятся один раз при открытии формы (например запрос в БД и отображение его в таблице), переопределяйте метод
DoCreate
(событиеOnCreate
) а не обработчикиOnShow
(будет выполнятся при каждом показе формы) илиOnActivate
(будет выполнятся при каждом входе в форму, хотя иногда именно такое и нужно для показа информации в реальном времени). А ещё лучше вынести этот функционал в отдельный публичный метод, напримерExecuteQuery
, и дёргать его после создания, как это делается например в диалогах открытия файлов. - В большинстве случаев, форму после закрытия нужно освобождать. Переопределяйте метод
DoClose
(событиеOnClose
) и устанавливайтеAction := caFree
. - Если вы всё таки используете форму как Одиночку, т.е. используете одну переменную объявленную в том же юните где и класс формы, то не забудьте поставить обработчик
OnDestroy
(да да, переопределить его не выйдет) а в нём установите значение переменной формы вnil
. Это спасёт вас от глюков в выражениях вродеif Assigned(Form1) then
. - Обработка Windows сообщений, типа
WM_MINIMIZE
и подобных, это большое зло. Панически избегайте их. - Про то что все таинственные названия, вроде
Form1
, нужно заменять на что-то более более внятное я даже писать не буду. - Я вас умоляю, избегайте модальных форм на всякую чепуху!
- ДА ТЫСЯЧИ ИХ!!!
- Не обращайтесь извне напрямую к компонентам формы, делайте свойства.
Выражение
FontSize := FontDialog.FontSize
лучше чем
FontSize := StrToInt(FontDialog.EditFontSize.Text)
Delphi: Используйте BPL пакеты вместо DLL библиотек!
Эта тема хорошо отхоливарена на SQL.RU. Вот основные цитаты оттуда передающие общий смысл этой статьи:
В DLL можно экспортировать только набор функций, если классы, наследование и прочие прелести ООП тогда BPL. Процедурное API в DLL устаревшее и скорее вредное.
Перестань парится фигней, форма в DLL это хак и будет это падать в самый не подходящий момент. Самое правильное — использовать Run-Time packages (для этого их и придумали).
Реальный пример
Была программа WorkTime, она использовала dll’ку admin.dll. Внутри это dll были всякие функции для соединения с БД и всякие формы настроек. Все функции которые она экспортирует были описаны в отдельном файле Admin_h.pas (типа Admin headers, «хашник»). Он был метров 8, потому что в него полностью скомпилировалась вся VCL библиотека и ещё куча бонусов.
Я перевёл эту dll в bpl пакет и поэкспериментировал с опциями компилировать пакет в программу или отдельно. Вышло вот что:
worktime без пакетов | 353 | |||
worktime полностью со всеми пакетами | 7630 | |||
admin.bpl | 2639 | |||
admin.dll | 8171 | |||
admin.dll без половины стандартных bpl | 4132 | |||
worktime через admin.dll | 6018 | + admin.dll | 8171 | = 14189 |
worktime через admin.bpl | 779.5 | + admin.bpl | 2639 | = 3418 |
Итого при переходе на bpl мы экономим: 14189 — 3418 = 10771 Kb = 10.5 Mb. Нехило.
Итак преимущества пакетов
- Пакет в отличии от DLL не чёрный ящик. Кликнув по функции ты попадёшь в тело функции а не в её объявление в хашнике.
- Пакет можно встроить в IDE. Классы внутри пакета можно оформить в виде компонентов и показать панели. Всё установка нужной нам фичи займёт три секунды на поиск компонента на палитре и тупо поставить компонент на форму. Текст программы становится меньше (и меньше ошибок).
- Можно использовать все типы и по полной.
- Перейти к человеческому ООП.
- Лучшая модульность.
- Экономия размера.
- Пакет можно скомпилировать прямо в программу, при этом неиспользуемые классы вырезаются компилятором и итоговое приложение занимает намного меньше места. И навсегда, навсегда избавится от DLL HELL!
- Можно сразу посмотреть любую функцию из пакета просто кликнув по ней и сразу попадёшь в исходники.
- Можно входить в тело функции из пакета в отладчике!
- Не нужно вести отдельный файл с заголовками (который может устареть, несоответсовать действительности).
- Не нужно описывать каждый тип в заголовочном файле.
- Не нужно парится и указывать тип передачи параметров (
stdcall
). Кстати и в обычной DLL над этим можно не парится если уверены заранее что не будут эти функции вызываться из программ на других языках (С++). - Не нужно парится и экспортировать функции.
- Не нужно парится и передавать в DLL переменную
Application
и ей подобные. - Можно использовать переменные из BPL’ки не передавая их через функции (например можно сходу использовать переменную
MainDatabase
а не извлекать её через функциюGetMainDatabase
. Можно будет сразу статически использовать её в формах и задавать её как свойство во всяких тамQuery
. - Из BPL’ки можно с чистой совестью использовать тип
string
. Настоящие DLL функции должны использовать толькоPСhar
, аналог Сишногосhar *
. Хотя добрый Borland сделал костыль в виде dll’киborlndmm.dll
и нужно обязательно в файле проекта программы подключить ПЕРВЫМShareMem
.
Неудобства
- В программе ты подключаешь не заголовочный файл а конкретный модуль (это нормально). Проблема на первых порах решается очень просто — там где раньше был модуль
Admin_h
вставляются все модули входящие в BPL а потом в CnPack’е вызывается команда убрать лишние модули. - Поскольку модули пакета находятся в том же адресном пространстве, нужно отслеживать глобальные переменные а точнее ИСКОРЕНИТЬ их вообще — это большое зло независимо от того dll это или пакет.
- BPL нельзя использовать другим программ написанным на других языках (на самом деле можно но…). В любом случае так как они у нас написаны их тоже нельзя использовтаь потому что функции должны использовать только базовые типы Си (
int
,double
,char *
). - BPL зависимы от версии делфи. Нас это не парит.
- BPL не могут ссылаться друг на друга (как и юниты), возможно придётся менять модульность.
- Пакеты нужно ставить в IDE. Это делается один раз и ничего сложного в этом нет. Вот только при изменении BPL пакета иногда приходится его переставлять в IDE чтобы изменёные компоненты появились на палитре.
- Геморок начинается если остальные DLL’ки настроены именно на DLL версию. Т.е. все зависимые DLL’ки тоже нужно переписать на BPL.
- Если компилируешь программу только с runtime пакетом
admin
, то пакеты которые у него в зависимостях тоже компилируются в runtime (Из-за этого возникают вопросы.).
Как переходить
Перевод на пакет у меня занял около полтора-двух часов, с перекурами.
- На первом этапе можно почти без особого гемороя сделать из DLL’ки BPL пакет при этом сохранив возможность компилировать и DLL.
- Берём исходники DLL. Все функции которые не экспортируются и глобальные переменные либо переносим в классы либо в отдельные модули которые не будут вставлятся в программу.
- По возможности при переносе функции используемые только одном классом делаем по человечески приватными. При этом если они на самом деле не используются компилятор выдаст предупреждение. При переносе
admin.dll
я так смог спокойно похезать с десяток невнятных и ненужных функций. Мусор есть мусор, и ООП позволяет с ним разобраться лучше всего. - Теперь у всех экспортируемые функции убираем нафиг директиву
stdcall
. - Тоже самое делаем для файла заголовков.
- У всех юнитов в названии добавляем имя dll, например был файл
PlanSh.pas
сталAdmin.PlanSh.pas
. Таким макаром мы избежим конфликтов имён юнитов в программах (например почти в каждой программе есть юнитGlobal_Variables
и внутри DLL’ки тоже есть такой). - Полученные исходники оформляем в пакет с BPL в конце названия (например
AdminBPL.dpk
). При этом мы сохранили DLL версиюAdmin.dpr
который может компилироваться в DLL. - Для удобства оба проекта объединить в группу проектов и компилить одним кликом.
- Теперь можно поставить в делфи этот пакет, и прописать в
SearchPath
исходники. - Отрываем программу которую будем переносить на BPL
- Проходимся по всем юнитам и тупо заменяем заголовочный файл DLL’ки на список всех модулей внутри неё (т.е. все модули которые входят в состав BPL)
- Поскольку мы хорошенько засрём пространство имён неиспользуемыми модулями делаем фокус: в CnPacke нажимаем кнопочку «Очистить неиспользуемые модули» он пройдётся и всё что не нужно просто молча удалит.
- Делаем билд программы и запускаем. В окошке EventLog убеждаемся что у нас загружается DLL (
admin.dll
). Грохаем DLL’ку - …
- PROFIT!!!
Второй этап
Второй этап можно начинать после того как все нужные DLL’ки и программы перенесены на BPL. Проект DLLки можно удалять за ненадобностью. Можно удалять функции типа имя_формы_show
а вместо них использовать нормальное создание форм через ИмяФормы := КлассФормы.Create(Application)
. А потом делать полноценный рефакторинг BPL по мере надобности.
Вывод
На BPL переходить нужно и несложно. Я ГАРАНТИРУЮ ЭТО! Они сильно, очень сильно облегчат жизнь повышая производительность, надёжность, удобство и уменьшая размер.