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

В предыдущей статье 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.

Заключение

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

Комментарии (2)


  1. HADGEHOGs
    22.12.2024 13:30

    Боже, лопни мои глазоньки.


  1. HADGEHOGs
    22.12.2024 13:30

    Отличный пример, как не надо писать статьи. Смешались в кашу кони-люди. Конечно, как бы пофиг, это Лазарус, свой мир, но оно бросает тень на мой любимый Delphi.

    Ребята - не верьте - это - не наши!