Lazarus IDE для аналитика. Приемы работы в современном Free Pascal — 2

от автора

Вместо вступления

В предыдущей статье Lazarus IDE для аналитика. Приемы работы в современном Free Pascal — 1 приведены приемы работы, связанные с базовым синтаксисом Free Pascal, в продолжении темы целесообразно привести материалы, касающиеся приемов работы и рекомендаций по ООП.

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

Структуры (Records)

Записи широко используются в Pascal для логической группировки элементов данных. Основное отличие между классами и записями заключается в том, что классы являются ссылочными типами, а записи — типами значений. Это означает, что оператор присваивания ведет себя по-разному для этих двух типов. Детальная информация о структурах представлена на Wiki странице Record.

Структуры без имени

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

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

type  TMyRecord = record   Name:string;   Integers:record       a:integer;       b:integer   end;   c:double;   end;

Сложные структуры (Advanced records)

Бывают ситуации когда необходимо чтобы работа с полями структуры строилась по подобию работы с полями для классов, т.е. чтобы у записи можно было создавать свойства, процедуры и функции. Для включения данной возможности необходимо добавить в директивы компилятора запись {$ModeSwitch advancedrecords}

В некоторых случаях может потребоваться хранить в поле записи не само значение, а ссылку на значение, определенное глобально, например если имеются данные, которые используется многими частями программы, и может быть полезно хранить ссылку на эти данные в записи, чтобы избежать их дублирования данных. Возможен вариант если данные, на которые нужно сослаться, могут изменяться в процессе выполнения программы, хранение ссылки на них позволит записи всегда иметь доступ к самой последней версии данных. В таких случаях понадобится включение директивы компилятора {$VarPropSetter+} и передача значения с иcпользованием constref.

Ниже приведен пример создания записи TMyRecord с закрытым полем FValue, свойством Value и функцией AddTen. Запись использует свойства для чтения и записи значения FValue, а также функцию для добавления числа 10 к значению FValue, кроме внутри записи организована работа с закрытым поле FPValue, хранящим ссылку на переменную со значением.

Пример программы AdvancedRecords
program demoadvancedrecordswithpointer;  {$mode objfpc}{$H+} {$ModeSwitch advancedrecords} {$VarPropSetter+}  type   TMyRecord = record   private     FPValue: ^Integer;     FValue:Integer;     function GetLinkedValue: Integer;     procedure SetLinkedValue(constref AValue: Integer);   public     property LinkedValue: Integer read GetLinkedValue write SetLinkedValue;     property Value: Integer read FValue write FValue;     function AddTen: Integer;     procedure AddFiveToLinkedValue;   end;  function TMyRecord.GetLinkedValue: Integer; begin   if Assigned(FPValue) then      result:=FPValue^   else      result:=0; end;  procedure TMyRecord.SetLinkedValue(constref AValue: Integer); begin   if FPValue=@AValue then Exit;   FPValue:=@AValue; end;  function TMyRecord.AddTen: Integer; begin   Result := FValue + 10; end;  procedure TMyRecord.AddFiveToLinkedValue; begin   if Assigned(FPValue) then      FPValue^ := FPValue^ + 5; end;  var   MyRec: TMyRecord;   GlobalValue:Integer; begin   GlobalValue:=10;   MyRec.LinkedValue :=GlobalValue;   MyRec.Value := GlobalValue;   WriteLn('LinkedValue: ', MyRec.LinkedValue);   WriteLn('Value: ', MyRec.Value);   GlobalValue:=20;   WriteLn('LinkedValue: ', MyRec.LinkedValue);   WriteLn('Value: ', MyRec.Value);   WriteLn('Value(AddTen): ', MyRec.AddTen);   MyRec.AddFiveToLinkedValue;   WriteLn('GlobalValue: ', GlobalValue);   ReadLn; end.

Хелперы (Helpers)

Класс Helper во Free Pascal и Delphi — это вспомогательный класс, который расширяет функциональность основного класса. Достаточно подробно об синтаксисе и возможностях Класс Helper изложено на Wiki странице — Helper types.

В целом использование такого «помощника» может быть полезно, когда вы хотите доработать функциональность существующего класса или компонента, не создавая новый. Например, когда вы хотите изменить поведение компонента TCheckBox, при отображении состояние элемента модели данных. В новых версиях Lazarus, при изменении свойства State у ТCheckBox, срабатывает событие OnChange и/или OnClick. Если вы хотите изменить это поведение, Класс Helper может в этом помочь. Он позволит добавить новое свойство, работа с которым позволит не запускать обработчик события. Таким образом, Helper позволит доработать стандартный компонент ТCheckBox, без необходимости создания нового компонента.

Ниже приведен пример реализации класса помощника

 TCheckBoxHelper = class helper for TCheckBox    procedure SetStateWithoutEvent(AState:TCheckBoxState);    public    property StateWithoutEvent:TCheckBoxState write SetStateWithoutEvent;   end;      ...   implementation      procedure TCheckBoxHelper.SetStateWithoutEvent(AState:TCheckBoxState);     var      onClickHandler : TNotifyEvent;      onChangeHandler : TNotifyEvent;     begin       onClickHandler := OnClick;       OnClick := nil;       onChangeHandler:= OnChange;       OnChange := nil;       State := AState;       OnClick:=onClickHandler;       OnChange:=onChangeHandler;   end; 

Helper для перечисления

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

Пример программы EnumWithCompactHelper
program EnumWithCompactHelper;  {$mode ObjFPC}{$H+} {$modeswitch typehelpers} {$codePage UTF-8}  Uses  Typinfo;     type   EFruitType = (ftApple, ftPineapple, ftPeach, ftCoconut);    { TFruitTypeHelper }   TFruitTypeHelper = type helper for EFruitType   private     const       Records: array[EFruitType, 1..3] of string = (             ( 'Яблоко', 'Яб', 'Сладкий красный фрукт' ),             ( 'Ананас', 'Анн', 'Тропический фрукт с колючей кожурой' ),             ( 'Персик', 'Пер', 'Сочный фрукт c большой косточкой' ),             ( 'Кокос', 'Кок', 'Большой орех с твердой скорлупой' )         );     function GetData(Index: Integer): string;   public     property ToString:string index 0 read GetData;     property Name: string index 1 read GetData;     property ShortName: string index 2 read GetData;     property Description: string index 3 read GetData;   end;  function TFruitTypeHelper.GetData(Index: Integer): string; begin   if Index <> 0 then    Result := Records[Self, Index]   else    Result :=GetEnumName(TypeInfo(EFruitType),ORD(Self)); end;  var   ft: EFruitType; begin   ft := ftApple;   WriteLn(ft.ToString);   WriteLn(ft.Name);   WriteLn(ft.ShortName);   WriteLn(ft.Description);   readln; end.

Helper для набора перечислений

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

Пример программы Setusage
program Setusage;  {$mode ObjFPC}{$H+} {$modeswitch typehelpers}  uses   Classes, SysUtils,Typinfo; type  TGridOption=(coOne,coTwo,coThre,coFive,coSix); TGridOptions = set of TGridOption;  // Хелпер, который поможет получить объект по индексу TGridOptionsHelper = type helper for TGridOptions    function GetItem(Index:integer):TGridOption;  public    property Items[Index:integer]:TGridOption read GetItem;   end;  function TGridOptionsHelper.GetItem(Index:integer):TGridOption; begin  for result in Self do   begin      if (Index=0) then         Exit;      Dec(Index);   end; end;  var AllowOptions:TGridOptions; Option:TGridOption;  begin   AllowOptions:=[coTwo,coFive,coSix];   Option:=AllowOptions.Items[1];   Writeln('AllowOptions.Items[1] = '+GetEnumName(TypeInfo(TGridOption),ORD(Option)));   Readln; end. 

Generic-и

В языке Free Pascal имеется встроенная библиотека — Free Generics Library или FGL, которая представляет собой нативную реализацию class templates, написанную в обобщенном синтаксисе Objfpc, примеры использования Generic приведены на Generics/ru — Free Pascal wiki.

В общем случае Generic конкретизируется для создания списка, словаря, дерева или графа определенного типа, что предоставляет удобный инструмент для управления набором элементов определенного типа. В стандартной библиотеке FGP имеется обобщенный тип TFGPObjecList, который удобно использовать и он имеет достаточно широкий набор функций для управления объектами списка и получения доступа к ним. Однако, иногда может потребоваться расширить возможности этого класса, добавив специальные поля или функции, чтобы расширить возможности и конкретизировать их под ваши нужды. В этом случае вы можете объявить класс-наследник для специализируемого класса и в приватной секции класса-наследника добавить набор полей, которые вам будут необходимы. Кроме того, вы можете дополнительно реализовать специальные функции, которые упростят взаимодействие с объектами этого списка.

Ниже приведен пример иллюстрирующий дополнение возможностей класса TFPGObjectList.

TDiskList = class (specialize TFPGObjectList)   private   FSelected:TDisk;   //Ссылка на выбранный диск,   function GetSelected:TDisk;   public   property Selected:TDisk read GetSelected write FSelected;   end;

Свойства

Свойство и ссылка на данные

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

Пример реализации приведен ниже:

{$VarPropSetter+}  type TSimpleClass = class  private   FPointerValue:^Integer;  SetValue(constref AInteger:integer);  GetValue:Integer;  public  constructor Create; overload;     destructor Destroy; override;  published     property SomeValue: integer read GetValue write SetValue; end;  implementation  // Код конструктора и деструктора   procedure TSimpleClass.SetValue(constref AInteger:integer);  begin  if FPointerValue &lt;&gt; @AInteger then    FPointerValue:=@AInteger;  end;    function TSimpleClass.GetValue:integer;  begin   if Assigned(FPointerValue) then      result:=FPointerValue^   else      result:=0;  end; end.

Индексы в свойствах

В языке программирования Free Pascal индексы используются для доступа к элементам массивов, но индексы могут быть использованы и для доступа к конкретному к элементам объекта. Для этих целей используется специальный синтаксис, который позволяет определить свойство с параметрами, например, property Cells[aCol, aRow: Integer]: string read GetCells write SetCells;.

Ниже приведен пример класса TTable с индексным свойством Cells, представляющим таблицу. Это свойство позволяет получать и устанавливать значения ячеек по координатам столбца (aCol) и строки (aRow).

type   TTable = class   private     FData: array of array of string;     function GetCells(aCol, aRow: Integer): string;     procedure SetCells(aCol, aRow: Integer; const Value: string);   public     property Cells[aCol, aRow: Integer]: string read GetCells write SetCells;   end;  function TTable.GetCells(aCol, aRow: Integer): string; begin   // Возвращаем значение ячейки по координатам   Result := FData[aCol, aRow]; end;  procedure TTable.SetCells(aCol, aRow: Integer; const Value: string); begin   // Устанавливаем значение ячейки по координатам   FData[aCol, aRow] := Value; end;  // Пример использования: var   MyTable: TTable; begin   MyTable := TTable.Create;   MyTable.Cells[1, 2] := 'Hello, World!'; // Установка значения ячейки   WriteLn(MyTable.Cells[1, 2]); // Получение значения ячейки end.

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

Колбэки (события) с дополнительными параметрами

События и обработчики позволяют реагировать на изменения внутри модели данных. Один из стандартных типов для обработчика событий — TNotifyEvent. Этот тип определяет метод, который будет вызван при возникновении события и в него обычно он передается ссылку на объект (Sender TObject). Однако иногда необходимо передавать в обработчик дополнительные параметры, например чтобы более точно идентифицировать изменения в модели данных.

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

type  TExtendedNotifyEvent = procedure(Sender: TObject; AdditionalData:string) of object;

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

procedure DoExtendedEvent(Sender: TObject; AdditionalData:string) begin endж

А в модели данных вызов обработчика событий будет иметь следующий вид:

unit MyUnit;  interface  uses   Classes, SysUtils;  type   TMyComponent = class(TComponent)   private     FOnExtendedNotify: TExtendedNotifyEvent;   public     property OnExtendedNotify: TExtendedNotifyEvent read FOnExtendedNotify write FOnExtendedNotify;     procedure DoSomething;   end;  implementation  procedure TMyComponent.DoSomething; begin   // ... Здесь расположена некая логика ...    // Генерация пользовательского события   if Assigned(FOnExtendedNotify) then     FOnExtendedNotify(Self, 'Дополнительная информация'); end;  end.

Делегаты

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

  1. Определение делегата: Делегат определяется как процедурный тип. Например:

    type   TMyDelegate = procedure(a, b: Integer) of object;
  2. Присваивание метода делегату: Делегату можно присвоить метод класса, который соответствует его сигнатуре. Например:

    var   MyDelegate: TMyDelegate;   MyClassInstance: TMyClass; begin   MyClassInstance := TMyClass.Create;   MyDelegate := @MyClassInstance.Add; end;
  3. Вызов метода через делегат: После присваивания метода делегату, его можно вызывать так же, как и обычный метод:

    MyDelegate(10, 5); // Вызов метода Add через делегат
Пример программы DelegateExample
program DelegateExample;  {$mode objfpc}{$H+}  type   TMyDelegate = procedure(a, b: Integer) of object;    TMyClass = class     procedure Add(a, b: Integer);     procedure Subtract(a, b: Integer);   end;  procedure TMyClass.Add(a, b: Integer); begin   WriteLn('Sum: ', a + b); end;  procedure TMyClass.Subtract(a, b: Integer); begin   WriteLn('Difference: ', a - b); end;  var   MyClassInstance: TMyClass;   MyDelegate: TMyDelegate;  begin   MyClassInstance := TMyClass.Create;   try     MyDelegate := @MyClassInstance.Add;     MyDelegate(10, 5); // Вывод: Sum: 15      MyDelegate := @MyClassInstance.Subtract;     MyDelegate(10, 5); // Вывод: Difference: 5   finally     MyClassInstance.Free;   end; end.

Взаимные ссылки между классами

В случае когда имеется какой-то список элементов, например TParent, который наследуется от TFPGObjectList. И нам хотелось бы чтобы класс TChild содержал ссылку на родителя TParent, для получения данных из него.

И здесь стоит упомянуть о возможности предварительного объявления. Строка TChild = class; как раз является предварительным объявлением (forward declaration) класса TChild. Она необходима, потому что TParent использует TChild в своем объявлении, а TChild использует TParent в своем. Без этой строки компилятор не будет знать о существовании класса TChild на момент объявления TParent, что приведет к ошибке компиляции Error: Identifier not found "TChild". Предварительное объявление позволяет компилятору узнать о существовании класса до его полного определения, что решает проблему взаимных ссылок между классами.

Пример модуля ParentChild
{$mode objfpc}{$H+} uses   fgl;  type   TChild = class;    TParent = class(specialize TFPGObjectList<TChild>)     private       FData: string;     public       constructor Create(AData: string);       function GetData: string;   end;    TChild = class     private       FParent: TParent;     public       constructor Create(AParent: TParent);       function GetParentData: string;       property Parent:TParent read FParent;   end;  constructor TParent.Create(AData: string); begin   inherited Create;   FData := AData; end;  function TParent.GetData: string; begin   Result := FData; end;  constructor TChild.Create(AParent: TParent); begin   FParent := AParent; end;  function TChild.GetParentData: string; begin   Result := FParent.GetData; end;  var   Parent: TParent;   Child, Child1: TChild; begin   Parent := TParent.Create('Привет из родителя');    Child := TChild.Create(Parent);   child1 :=  TChild.Create(Parent);   Parent.Add(Child);   Child.Parent.Add(Child1);   // Вывод данных родителя из элементов списка делегат   WriteLn(Parent[0].GetParentData);   WriteLn(Parent[1].GetParentData);   // Очистка   Parent.Free;   readln; end.

Интерфейсы, если взаимные ссылки не работают

В случае когда для каждого класса создается свой модуль, то попытка использовать предварительное объявление приведет к неразрешимой на данном этапе ошибке перекрёстного использования модулей error: circular unit reference between unit1 and unit2. и в этом случае на помощь могут прийти интерфейсы. Как правило, в классе TChild нам не нужно получать все возможные данные из класса TParent, нужен доступ ко определенным свойствам или функциям. Такие функции мы можем заранее определить в интерфейсе, и применить в классе TChild , а реализацию таких функций обеспечит TParent. Подробную информацию об интерфейсах можно получить из материалов Краткое введение в современный Object Pascal для программистов (castle-engine.io) — Интерфейсы

Пример модулей:

ParentUnit.pas
unit ParentUnit;  {$mode objfpc}{$H+}  interface  type   IParent = interface     function GetData: string;   end;    TParent = class(TInterfacedObject, IParent)   private     FData: string;   public     constructor Create(AData: string);     function GetData: string;   end;  implementation  constructor TParent.Create(AData: string); begin   inherited Create;   FData := AData; end;  function TParent.GetData: string; begin   Result := FData; end; end.

ChildUnit.pas
unit ChildUnit;  {$mode objfpc}{$H+}  interface  uses   ParentUnit;  type   TChild = class   private     FParent: IParent;   public     constructor Create(AParent: IParent);     function GetParentData: string;     property Parent: IParent read FParent;   end;  implementation  constructor TChild.Create(AParent: IParent); begin   FParent := AParent; end;  function TChild.GetParentData: string; begin   Result := FParent.GetData; end; end.

MainUnit.pas
program MainUnit;  {$mode objfpc}{$H+}  uses   ParentUnit, ChildUnit;  var   Parent: IParent;   Child, Child1: TChild; begin   Parent := TParent.Create('Привет из родителя');    Child := TChild.Create(Parent);   Child1 := TChild.Create(Parent);    // Вывод данных родителя из элементов списка делегат   WriteLn(Child.GetParentData);   WriteLn(Child1.GetParentData);    // Очистка   ReadLn; end.

Заключение

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


ссылка на оригинал статьи https://habr.com/ru/articles/868720/