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;

Но есть пару нюансов:

  1. типами аргументов у нас могут быть только примитивные Integer, Int64, Double и string. С объектами или перечисляемыми типы облом 😦
  2. Значения аргументов функции вычисляются перед её вызовом, поэтому в отличии от стандартного условного оператора 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.

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

  1. Если вычисления в аргументах IfThen довольно сложные или запись IfThеn не вмещается на одну строчку, перепишите этот код через стандартный блок if-then-else.
  2. Никогда, никогда не делайте вложенные вызовы 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 (Из-за этого возникают вопросы.).

Как переходить

Перевод на пакет у меня занял около полтора-двух часов, с перекурами.

  1. На первом этапе можно почти без особого гемороя сделать из DLL’ки BPL пакет при этом сохранив возможность компилировать и DLL.
  2. Берём исходники DLL. Все функции которые не экспортируются и глобальные переменные либо переносим в классы либо в отдельные модули которые не будут вставлятся в программу.
  3. По возможности при переносе функции используемые только одном классом делаем по человечески приватными. При этом если они на самом деле не используются компилятор выдаст предупреждение. При переносе admin.dll я так смог спокойно похезать с десяток невнятных и ненужных функций. Мусор есть мусор, и ООП позволяет с ним разобраться лучше всего.
  4. Теперь у всех экспортируемые функции убираем нафиг директиву stdcall.
  5. Тоже самое делаем для файла заголовков.
  6. У всех юнитов в названии добавляем имя dll, например был файл PlanSh.pas стал Admin.PlanSh.pas. Таким макаром мы избежим конфликтов имён юнитов в программах (например почти в каждой программе есть юнит Global_Variables и внутри DLL’ки тоже есть такой).
  7. Полученные исходники оформляем в пакет с BPL в конце названия (например AdminBPL.dpk). При этом мы сохранили DLL версию Admin.dpr который может компилироваться в DLL.
  8. Для удобства оба проекта объединить в группу проектов и компилить одним кликом.
  9. Теперь можно поставить в делфи этот пакет, и прописать в SearchPath исходники.
  10. Отрываем программу которую будем переносить на BPL
  11. Проходимся по всем юнитам и тупо заменяем заголовочный файл DLL’ки на список всех модулей внутри неё (т.е. все модули которые входят в состав BPL)
  12. Поскольку мы хорошенько засрём пространство имён неиспользуемыми модулями делаем фокус: в CnPacke нажимаем кнопочку «Очистить неиспользуемые модули» он пройдётся и всё что не нужно просто молча удалит.
  13. Делаем билд программы и запускаем. В окошке EventLog убеждаемся что у нас загружается DLL (admin.dll). Грохаем DLL’ку
  14. PROFIT!!!

Второй этап

Второй этап можно начинать после того как все нужные DLL’ки и программы перенесены на BPL. Проект DLLки можно удалять за ненадобностью. Можно удалять функции типа имя_формы_show а вместо них использовать нормальное создание форм через ИмяФормы := КлассФормы.Create(Application). А потом делать полноценный рефакторинг BPL по мере надобности.

Вывод

На BPL переходить нужно и несложно. Я ГАРАНТИРУЮ ЭТО! Они сильно, очень сильно облегчат жизнь повышая производительность, надёжность, удобство и уменьшая размер.

Матчасть