В данной статье мы рассмотрим проблему, возникшую при использовании вложенных и generic классов в FPC 3.2.2 (Free Pascal Compiler) и предложим решение, основанное на опыте, изложенном в обсуждении на форуме. Проблема проявляется как исключение, связанное с управлением памятью, при работе с перечислителем (enumerator) для generic списка.
Описание проблемы
Автор столкнулся с исключением при попытке итерировать по списку, созданному с использованием generic класса TList<T1> с вложенным классом Enumerator и ReverseEnumerator. Исключение возникало в строке кода, где происходило обращение к свойству Current перечислителя.
Исходный код, вызывавший проблему
{$mode objfpc}{$H+}
{$modeswitch generics}
unit Container;
interface
type
generic TList<T1> = class
private
FValue : T1;
FTop : specialize TList<T1>;
FBottom : specialize TList<T1>;
FPrev : specialize TList<T1>;
FNext : specialize TList<T1>;
FCount : Integer;
type
Enumerator = class
private
FCurrent : specialize TList<T1>;
FHead : specialize TList<T1>;
public
constructor Create(AList: specialize TList<T1>);
function GetCurrent: T1;
function MoveNext: Boolean;
property Current: T1 read GetCurrent;
end;
ReverseEnumerator = class
private
FCurrent: specialize TList<T1>;
public
constructor Create(AList: specialize TList<T1>);
function MoveNext: Boolean;
function GetCurrent: T1;
property Current: T1 read GetCurrent;
end;
public
constructor Create(AValue: T1);
procedure Add(AValue: T1);
function GetEnumerator: Enumerator;
function GetReverseEnumerator: ReverseEnumerator;
published
property Count: Integer read FCount;
end;
generic TListVector<T1> = class(specialize TList<T1>)
public
constructor Create(AValue: T1);
end;
generic TMap<T1, T2> = class
private
FValue1 : T1;
FValue2 : T2;
FCount : Integer;
public
constructor Create(AValue1: T1; AValue2: T2);
end;
implementation
{ TListEnumerator }
constructor TList.Enumerator.Create(AList: specialize TList<T1>);
begin
inherited Create;
FCurrent := nil;
FHead := AList.FTop;
end;
function TList.Enumerator.MoveNext: Boolean;
begin
if FCurrent = nil then
FCurrent := FHead
else
FCurrent := FCurrent.FNext;
result := FCurrent <> nil;
end;
function TList.Enumerator.GetCurrent: T1;
begin
Result := FCurrent.FValue;
end;
{ TListReverseEnumerator }
constructor TList.ReverseEnumerator.Create(AList: specialize TList<T1>);
begin
inherited Create;
FCurrent := AList.FBottom; // Beginne ganz unten
end;
function TList.ReverseEnumerator.MoveNext: Boolean;
begin
Result := FCurrent <> nil;
if Result then
FCurrent := FCurrent.FPrev;
result := FCurrent <> nil; // Ошибка здесь!
end;
function TList.ReverseEnumerator.GetCurrent: T1;
begin
Result := FCurrent.FValue;
end;
{ TListVector }
constructor TListVector.Create(AValue: T1);
begin
inherited Create(AValue);
end;
{ TList }
constructor TList.Create(AValue: T1);
begin
inherited Create;
FValue := AValue;
FTop := self;
FBottom := self;
FNext := nil;
FCount := 1;
end;
procedure TList.Add(AValue: T1);
var
tmp: specialize TList<T1>;
begin
tmp := TList.Create(AValue);
FBottom.FNext := tmp;
FBottom := tmp;
inc(FCount);
end;
function TList.GetEnumerator: Enumerator;
begin
result := Enumerator.Create(Self);
end;
function TList.GetReverseEnumerator: TList.ReverseEnumerator;
begin
result := ReverseEnumerator.Create(Self);
end;
constructor TMap.Create(AValue1: T1; AValue2: T2);
begin
inherited Create;
end;
end.
{$mode objfpc}{$H+}
{$M-}
{$define DLLIMPORT}
program test;
uses
Container;
type
TVector = specialize TListVector< Integer >;
TVectorReverse = TVector.ReverseEnumerator;
var
V : TVector;
REnum : TVectorReverse;
begin
V := TVector.Create(12);
V.Add(2);
V.Add(3);
V.Add(4);
REnum := V.GetReverseEnumerator;
writeln('reeee');
while REnum.MoveNext do
writeln('Value: ', intToStr(REnum.Current));
REnum.Free;
V.Free;
end.
Решение
Проблема заключалась в логике работы метода MoveNext класса ReverseEnumerator. В оригинальном коде, если FCurrent указывал на первый элемент списка (последний при обратном перечислении), Result устанавливался в True, а затем FCurrent обнулялся. В результате, при следующем вызове GetCurrent происходило обращение к nil, что приводило к исключению.
Предложенное решение заключается в изменении логики MoveNext для ReverseEnumerator следующим образом:
constructor TList.ReverseEnumerator.Create(AList: specialize TList<T1>);
begin
inherited Create;
FCurrent := AList.FBottom; // Beginne ganz unten
FStart := false;
end;
function TList.ReverseEnumerator.MoveNext: Boolean;
begin
if not FStart then
begin
FStart := true;
result := FCurrent <> nil;
end else
begin
if FCurrent <> nil then
FCurrent := FCurrent.FPrev;
result := FCurrent <> nil;
end;
end;
В этом исправленном коде добавлена переменная FStart: Boolean, которая контролирует, был ли уже выполнен первый шаг итерации. При первом вызове MoveNext, FStart равна false, поэтому устанавливается в true и возвращается true, если FCurrent не равен nil. В последующих вызовах MoveNext происходит переход к предыдущему элементу и проверка на nil.
Альтернативное решение
Более элегантным решением было бы приведение логики ReverseEnumerator к логике Enumerator, как и было предложено в обсуждении:
constructor TList.ReverseEnumerator.Create(AList: specialize TList<T1>);
begin
inherited Create;
FCurrent := nil;
FHead := AList.FBottom;
end;
function TList.ReverseEnumerator.MoveNext: Boolean;
begin
if FCurrent = nil then
FCurrent := FHead
else
FCurrent := FCurrent.FPrev;
result := FCurrent <> nil;
end;
Это решение полностью соответствует логике прямого перечислителя, только начинает с конца списка.
Дополнительные замечания
Приватность вложенных типов: Важно помнить, что вложенные типы, объявленные в секции private, имеют область видимости уровня модуля (unit scope), а не класса. Это означает, что к ним можно получить доступ из любого места в том же модуле, даже вне класса, в котором они определены. Для ограничения видимости на уровне класса следует использовать strict private.
Версии компилятора: Автор отметил, что проблема возникала в FPC 3.3.1, но не в FPC 3.2.2. Это подчеркивает важность использования стабильных версий компилятора и проверки кода на разных версиях для выявления потенциальных ошибок.
Отладка: При возникновении подобных проблем рекомендуется использовать отладчик (например, GDB) для пошагового выполнения кода и анализа значений переменных, чтобы точно определить место возникновения ошибки.
Заключение
Проблема с памятью, возникшая при использовании вложенных и generic классов в FPC 3.2.2, была вызвана ошибкой в логике работы обратного перечислителя. Предложенные решения, включающие исправление логики MoveNext и приведение ее к логике прямого перечислителя, позволяют избежать исключения и корректно итерировать по списку в обратном порядке. Важно помнить о нюансах приватности вложенных типов и использовать стабильные версии компилятора для разработки надежного кода.
В статье рассматривается и предлагается решение проблемы с управлением памятью в FPC 3.2.2, возникающей при использовании вложенных и generic классов в Delphi и Pascal, проявляющейся в исключении при итерации по generic списку в обратном порядке.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS