Поиск по блогу

четверг, 16 декабря 2010 г.

Delphi: работа с классами, унаследованными от TList

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

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

Самый простой пример такой объектной организации:
TBASetting  = class
BA : string;
MM_CONST : integer;
STRIKESTEP,
MIN_STRIKE,
MAX_STRIKE : double;
public
constructor Create(const aPath: string; const aParams: TStrings); overload;
...
end;

TBASettings = class(TList)
...
function LoadFromINI (IniFile : TIniFile) : boolean;
function SaveToINI (IniFile: TIniFile) : boolean;
procedure Clear; override;
end;

Остановлюсь подробнее на следующих пунктах:
- создание объектов, заполнение списка;
- освобождение памяти при "очистке" списка.

Создание объектов и добавление их в список


Опять же, два варианта.
1. Создание одиночного объекта (выделение памяти, присвоение значений полям), добавление в список.
tmpSet := TBASetting.Create;
tmpSet.BA := sBA;
tmpSet.MM_CONST := iMM_CONST;
tmpSet.STRIKESTEP := dSTRIKESTEP;
tmpSet.MIN_STRIKE := dMIN_STRIKE;
tmpSet.MAX_STRIKE := dMAX_STRIKE;
BASettings.Add(tmpSet);


2. Массовое заполнение списка объектами (групповое добавление объектов). Этот вариант удобен, если, например, считывать данные из ini-файла или XML.
function TBASettings.LoadFromINI(IniFile: TIniFile): boolean;
var
Sections,
SecParams : TStringList;
i : integer;
sVal : string;
begin
Result := false;
Sections := TStringList.Create;
try
IniFile.ReadSections(Sections);
SecParams := TStringList.Create;
try
for i:=0 to Sections.Count-1 do
begin
sVal := Sections[i];
// ну, тут всякие условия, проверки, код для "отсева" и т.д.
if sVal <> 'COMMON' then
begin
IniFile.ReadSectionValues(sVal, SecParams);
if SecParams.Count > 0 then
begin
BASettings.Add(TBASetting.Create(sVal, SecParams));
end
end;
end;
finally
SecParams.Free;
end;
finally
Sections.Free;
end;
Result := true;
end;

constructor TBASetting.Create(const aPath: string;
const aParams: TStrings);
begin
BA := aPath;
MM_CONST := StrToIntDef(aParams.Values['MM_CONST'], 4);
STRIKESTEP := StrToFloatDef(aParams.Values['STRIKESTEP'], 0);
MIN_STRIKE := StrToFloatDef(aParams.Values['MIN_STRIKE'], 0);
MAX_STRIKE := StrToFloatDef(aParams.Values['MAX_STRIKE'], 0);
end;


Очистка списка объектов (наследника TList)


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

Что делать при удалении всего списка? У TList есть метод Free, который освобождает память и удаляет объект. Казалось бы, надо у нашего списка перегрузить этот метод? Но правильнее перегрузить метод Clear (который, в свою очередь, вызывается из Free, а Free вызывается FreeAndNil-ом).

TBASettings = class(TList)
...
function LoadFromINI (IniFile : TIniFile) : boolean;
function SaveToINI (IniFile: TIniFile) : boolean;
procedure Clear; override;
end;

procedure TBASettings.Clear;
var
i : integer;
tmpSet : TBASetting;
begin
for i := 0 to Count-1 do
begin
tmpSet := TBASetting(Items[i]);
tmpSet.Free;
end;
inherited;
end;


Если в составе класса в качестве полей есть экземпляры других классов, то надо:
- перегрузить конструктор, выделить в нем память под эти поля;
- перегрузить деструктор объекта, в котором освободить память, выделенную при создании.

Листинг для примера:
TBASetting  = class
BA : string;
PUTNAME,
CALLNAME : TStringList;
MM_CONST : integer;
STRIKESTEP,
MIN_STRIKE,
MAX_STRIKE : double;
public
...
constructor Create(const aPath: string; const aParams: TStrings); overload;
constructor Create; overload;
destructor Destroy; override;
end;

constructor TBASetting.Create;
begin
PUTNAME := TStringList.Create;
CALLNAME := TStringList.Create;
end;

destructor TBASetting.Destroy;
begin
PUTNAME.Free;
CALLNAME.Free;
end;

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

На сегодня это все, о чем я хотела поведать. Пойду дальше программировать :)

P.S.: Производительность труда заметно повышается, когда дома ломается компьютер. На работе приходится брать себя в руки и работать! :D
___

Чтобы быть в курсе обновлений блога, можно подписаться на RSS.

Статьи схожей тематики:



9 комментариев:

  1. В таком случае лучше использовать TObjectList:
    1) элементы списка - объекты (TObject), а не указатели (Pointer).
    2) при очистке он освобождает каждый элемент списка автоматически.

    А еще лучше - использовать современные версии Delphi: 2009, 2010, ХЕ. Там есть дженерики.

    PS. Производительность труда возрастает еще больше, если использовать современные инструменты :)

    ОтветитьУдалить
  2. Полностью согласен с Frantic по поводу TObjectList вместо TList.

    Но хотел бы ещё напомнить о старой конструкции типа:
    var
    Mas: array of
    record
    AA: Integer;
    BB: Integer;
    end;

    Плюсы коротко и быстро(быстрее чем TList).
    Минусы не ООП (Но в вашем примере объекты одного класса и не имеют методов кроме Создания, только поля).

    Юрий

    ОтветитьУдалить
  3. Так использовать наследование плохо - нарушаем принцип Лисков и любой проект размером больше маленького за такое жестоко накажет.
    Если нужен специальный список, то кошерно использовать композицию - вставить TList внутрь в виде приватного поля и делегировать к нему все нужные для специфического списка вызовы.

    ОтветитьУдалить
  4. Frantic, да, я вообще торможу с освоением новых инструментов) У меня до сих пор Delphi 7 (просто потому, что на работе приходится иметь дело с семеркой).

    По поводу TObjectList - да, согласна, но привычка - великая вещь.)

    Юрий, если я использую список объектов, то мне нужно именно ООП. Я просто в листинге потерла все ненужные процедуры/функции.

    Кирилл, пошла гуглить про принцип Лисков :)

    Спасибо за комментарии! Буду умнеть под чутким руководством общественности :) Авось вырасту когда-нибудь из баз данных... :D

    ОтветитьУдалить
  5. Странно, почему для удаления списка надо перекрывать метод Clear. А если надо удалить I-й элемент?

    В TList специально для таких случаев есть метод Notify. В Вашем случае будет что-то типа такого:

    procedure TBASettings.Notify(Ptr: Pointer; Action: TListNotification);
    begin
    if Action = lnDeleted then TBASetting(Ptr).Free;
    inherited Notify(Ptr, Action);
    end;

    ОтветитьУдалить
  6. Еще нюанс: можно избежать использования вложенных try..finally

    function TBASettings.LoadFromINI(IniFile: TIniFile): boolean;
    var
    Sections,
    SecParams : TStringList;
    i : integer;
    sVal : string;
    begin
    Result := false;
    SecParams := nil;
    Sections := TStringList.Create;
    try
    IniFile.ReadSections(Sections);
    SecParams := TStringList.Create;
    for i:=0 to Sections.Count-1 do
    begin
    sVal := Sections[i];
    // ну, тут всякие условия, проверки, код для "отсева" и т.д.
    if sVal <> 'COMMON' then
    begin
    IniFile.ReadSectionValues(sVal, SecParams);
    if SecParams.Count > 0 then
    begin
    BASettings.Add(TBASetting.Create(sVal, SecParams));
    end
    end;
    end;
    finally
    FreeAndNil(SecParams);
    FreeAndNil(Sections);
    end;
    Result := true;
    end;

    ОтветитьУдалить
  7. По поводу цикла для очистки элементов списка. Обычно делают что то типа

    for i:=List.Count-1 downto 0 do
    List[i].Free

    так меньше вызовов Count и нет головной боли относительно - список заметит удаление элемента и сожмется или нет.

    ОтветитьУдалить
  8. > так меньше вызовов Count и нет головной боли относительно - список заметит удаление элемента и сожмется или нет.
    Что за бред?
    1. Count в любом цикле for (и по возрастанию, и по убыванию) вызовется один раз
    2. "Сжатие" списка во время его очистки невозможно, т.к. приложение однопоточное. Если потоков несколько - то работа со списком идет в критической секции => в каждый момент времени поток один. Если критической секции (или другого метода синхронизации) нет - то программист идиот и такая программа будет глючить не только при очистке, а вообще повсюду.

    ОтветитьУдалить
  9. Не совсем понятно, почему отношение "реализуется посредством" фактически вылилось в вашем случае в наследование TList, у которого виртуальных методов-то всего два - Error и Notify... Логично применять такое наследование в тех случаях, когда вам остро необходим доступ к protected-мемберам, но здесь такой необходимости не видно. Поэтому, в общем случае, как мне кажется, лучше было бы применить в подобной ситуации агрегацию. Вы ведь не порождаете некий новый последовательный контейнер со специфичным поведением, а делаете какой-то менеджер настроек Вашего приложения. Менеджер по своей сути - явно не список, и как он хранит там у себя внутри настройки - сугубо его дело, то есть то, что называется деталями реализации. Завтра потребуется (заказчик так захотел, например, API какое-то там прикрутить надо) хранить настройки в древовидной структуре - менеджер настроек станет деревом? А если в БД - ему что, датасетом стать прикажете?
    Что касается вот этого:
    >for i:=List.Count-1 downto 0 do
    >List[i].Free
    и того, что список что-то там заметит - так извините: ничего список не заметит, просто потому, что вы с ним ничего здесь не делаете. Во-первых, нужно понимать, что есть такое TList. Это классовая обертка над массивом указателей. Неважно на что - void * некий. Соответственно, TList хранит указатели, и ни о чем другом, ни о каких этих ваших объектах или чем-то там еще, на чем вы пытаетесь позвать метод Free, он понятия не имеет. А во-вторых, посмотрите код Classes.pas. По своей сути, TList - он типа сильно упрощенного std::vector. Размер памяти, выделенной для хранения его элементов, изменяется только в методе TList.SetCapacity, который зовется при добавлении элементов в список и при вызове метода Clear. Он не вызывается ни при Remove, ни при Delete.
    Хотите "сжимать" ваш список при убиении объектов, которые Вы туда положили - занулите указатели на них в списке и позовите метод Pack, он для того и сделан. Вот только размер памяти, занимаемой списком при этом не уменьшится по причинам, выше уже упомянутым.

    ОтветитьУдалить

Комментарии модерируются, вопросы не по теме удаляются, троллинг тоже.

К сожалению, у меня нет столько свободного времени, чтобы отвечать на все частные вопросы, так что, может, свой вопрос лучше задать на каком-нибудь форуме?

Поделиться