Вместо вступления
В предыдущей статье 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.
Заключение
Вместо традиционного обощающего заключения хотелось бы обратить ваше внимание еще на один аспект работы над проектом - это Стилевое оформлении кода. Стиль кодирования может отличаться в зависимости от языка программирования и конкретных требований проекта. Однако важно следовать стандартам стиля кодирования для обеспечения читаемости и понимания вашего кода другими разработчиками.
Ниже приведены ресурсы на которых можно получить информацию о стилевом оформлении кода:
Комментарии (2)
HADGEHOGs
22.12.2024 13:30Отличный пример, как не надо писать статьи. Смешались в кашу кони-люди. Конечно, как бы пофиг, это Лазарус, свой мир, но оно бросает тень на мой любимый Delphi.
Ребята - не верьте - это - не наши!
HADGEHOGs
Боже, лопни мои глазоньки.