Вместо вступления
В предыдущей статье 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 <> @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 представляют собой типы, которые могут ссылаться на методы или процедуры. Они позволяют динамически назначать методы и вызывать их через делегаты, что делает код более гибким и модульным.
-
Определение делегата: Делегат определяется как процедурный тип. Например:
type TMyDelegate = procedure(a, b: Integer) of object;
-
Присваивание метода делегату: Делегату можно присвоить метод класса, который соответствует его сигнатуре. Например:
var MyDelegate: TMyDelegate; MyClassInstance: TMyClass; begin MyClassInstance := TMyClass.Create; MyDelegate := @MyClassInstance.Add; end;
-
Вызов метода через делегат: После присваивания метода делегату, его можно вызывать так же, как и обычный метод:
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/
Добавить комментарий