Social Icons

пятница, 29 июля 2011 г.

Используем дженерики в Delphi! - Часть 2 (Системные классы)

[Содержание]
[Часть 1 - Введение в дженерики] [Часть 2 - Системные классы] [Часть 3 - Приложение]

  1. Введение
  2. Generics.Defaults
    1. TComparer<T>
  3. Generics.Collections
    1. TArray
      1. Сортировка и поиск элементов в одномерном массиве
      2. Сортировка двумерного массива
    2. TDictionary<T> и TObjectDictionary<T>
      1. Работа со словарем (на примере TDictionary<T>)
      2. События OnKeyNotify и OnValueNotify
      3. Ключи, значения и TArray
    3. TList<T> и TObjectList<T>
      1. Работа со списком (на примере TObjectList<T>)
      2. Поиск и сортировка
      3. Событие OnNotify
    4. TStack<T> и TObjectStack<T>
    5. TQueue<T> и TObjectQueue<T>
  4. Заключение

1. Введение
Рассмотрим стандартные обощенные классы в Delphi и их функциональные особенности.
Большинство информации будет представлено в виде демо-примеров, которые при желании можно будет легко воспроизвести у себя на ПК.

2. Generics.Defaults
2.1. TComparer<T>
TComparer является базовым обобщенным классом, реализует интерфейс IComparer и предназначен для создания компараторов - классов, отвечающих за сравнение других классов или типов.
Создавать компараторы для некоторых стандартных типов (о которых можно получить информацию через RTTI) очень просто: для этого существует классовая функция TComparer.Default. Так, компаратор для целых чисел или строк создается через TComparer<Integer>.Default и TComparer<string>.Default соотвественно. А как же сранивать нестандартные типы или собственные классы? Для этого потребуется создать собственный компаратор на основе TComparer<T> и всего лишь переопределить функцию Compare:
Листинг 11 - Создание пользовательского компаратора для класса TCustomer
uses
  SysUtils, Generics.Defaults;
 
...

  // Создадим компаратор для класса TCustomer из Листинга 4

  // Немного расширим класс, пусть у клиента будет банковский счет
  TCustomer = class
  private
    FName: string;
    FMoney: Currency;
  private
    constructor Create(const Name: string; Money: Currency);
    property Name: string read FName write FName;
    property Money: Currency read FMoney write FMoney;
  end;

  // Объявляем наш компаратор и переопределяем функцию Compare
  TCustomerComparer = class(TComparer<TCustomer>)
    function Compare(const Left, Right: TCustomer): Integer; override;
  end;


function TCustomerComparer.Compare(const Left, Right: TCustomer): Integer;
begin
  // Переопределяемый метод Compare должен возвращать:
  // 0 - при равенстве значений
  // >0 если первый параметр больше второго (Left > Right)
  // <0 в остальных случаях (Left < Right)

  // Будем сортировать клиентов сначала по имени, а затем по сумме счета
  Result := CompareStr(Left.Name, Right.Name);
  if Result = 0 then
    Result := Left.Money - Right.Money;
end;

...
Кроме того, можно и не создавать класс, а использовать классовую функцию - TComparer<T>.Construct в том случае, когда компаратор передается как параметр метода. Сделать это необходимо через анонимную функцию того же вида, что и рассмотренная выше Compare:
Листинг 12 - "Конструирование" пользовательского компаратора с использованием функции Construct и анонимного метода
...

  // Предположим, что метод Sort в TCustomersManager принимает компаратор
  // в качестве параметра для сортировки имеющихся клиентов
  TCustomersManager.Sort(TComparer<TCustomer>.Construct(
    function (const Left, Right: TCustomer): Integer
    begin
      Result := CompareStr(Left.Name, Right.Name);
      if Result = 0 then
        Result := Left.Money - Right.Money;
    end));

...
Где и как использовать компараторы в различных вариациях, мы увидим уже в следующих разделах.

3. Generics.Collections
3.1. TArray
Создавать экземпляр этого класса не нужно. TArray содержит 2 статических метода:
  • Sort. Использует алгоритм быстрой сортировки, может принимать компаратор в качестве параметра
  • BinarySearch. Ищет элемент в массиве и возвращает True если находит. Также принимает компаратор, требует, чтобы массив был предвариательно отсортирован
3.1.1. Сортировка и поиск элементов в одномерном массиве
Рассмотрим возможности сортировки и поиска на примере простого одномерного массива.
Листинг 13 - Сортировка и поиск элементов одномерного целочисленного массива с использованием TArray
...

uses
  SysUtils, Types,
  Generics.Collections, Generics.Defaults;

function CompareIntReverse(const Left, Right: Integer): Integer;
begin
  // Сравниваем элементы "наоборот" и получаем обратный порядок
  Result := Right - Left;
end;

procedure PrintMatrix(A: TIntegerDynArray);
var
  item: Integer;
begin
  for item in A do
    Write(item, ' ');
  Writeln; Writeln;
end;

var
  A: TIntegerDynArray;
  FoundIndex: Integer;

begin

  ...

  SetLength(A, 5);
  A[0] := 1;
  A[1] := 6;
  A[2] := 3;
  A[3] := 2;
  A[4] := 9;

  // Распечатаем, что есть
  Writeln('Исходный массив:');
  PrintMatrix(A);

  // Сортируем по возрастанию без компаратора
  TArray.Sort<Integer>(A);
  Writeln('По возрастанию Sort без параметров:');
  PrintMatrix(A);

  // Сортируем по убыванию, конструируя компаратор
  // с помощью анонимного метода
  TArray.Sort<Integer>(A, TComparer<Integer>.Construct(
    function (const Left, Right: Integer): Integer
    begin
      Result := Right - Left;
    end));
  Writeln('По убыванию c TComparer<Integer>.Construct(анонимный метод):');
  PrintMatrix(A);

  // Опять сортируем по возрастанию c применением компаратора по умолчанию
  TArray.Sort<Integer>(A, TComparer<Integer>.Default);
  Writeln('По возрастанию c TComparer<Integer>.Default:');
  PrintMatrix(A);

  // И снова по убыванию с использованием собственного компаратора
  TArray.Sort<Integer>(A,  TComparer<Integer>.Construct(CompareIntReverse));
  Writeln('По убыванию TComparer<Integer>.Construct(CompareIntReverse):');
  PrintMatrix(A);

  // Выполняем поиск несуществующего элемента
  Writeln('BinarySearch несуществующего элемента');
  if TArray.BinarySearch<Integer>(A, 5, FoundIndex) then
    Writeln('5-ка найдена, ее индекс ', FoundIndex)
  else
    Writeln('5-ки в массиве нет!');
  Writeln;

  // Выполняем поиск существующего элемента
  Writeln('BinarySearch существующего элемента');
  if TArray.BinarySearch<Integer>(A, 6, FoundIndex) then
    Writeln('6-ка найдена, ее индекс ', FoundIndex)
  else
    Writeln('6-ки в массиве нет!');
  Writeln;

  // Выполняем поиск c нашим компаратором CompareIntReverse
  Writeln('BinarySearch существующего элемента с компаратором');
  if TArray.BinarySearch<Integer>(A, 6, FoundIndex,
    TComparer<Integer>.Construct(CompareIntReverse)) then
    Writeln('6-ка найдена, ее индекс ', FoundIndex)
  else
    Writeln('6-ки в массиве нет!');
  Writeln;

  FreeAndNil(A);


...
Результат работы приведен на Рисунке 3:

Рисунок 3 - Результаты поиска и сортировки с использованием TArray.Sort и TArray.BinarySearch
3.1.2. Сортировка двумерного массива
Посмотрим, как осуществить сортировку двумерного массива:
Листинг 14 - Сортировка элементов двумерного целочисленного массива с использованием TArray
...

uses
  SysUtils, Math, Types,
  Generics.Collections,
  Generics.Defaults;

type
  TDoubleIntegerArray = array of TIntegerDynArray;

procedure PrintMatrix(A: TDoubleIntegerArray);
var
  i, j: Integer;
begin
  for i := Low(A) to High(A) do
  begin
    for j := Low(A[0]) to High(A[0]) do
      Write(A[i, j]: 3, ' ');
    Writeln;
  end;
  Writeln; Writeln;
end;

var
  A: TDoubleIntegerArray;
  FoundIndex: Integer;
  i, j: Integer;

begin

...

  // Заполним целочисленный массив случайными числами [1..50]
  SetLength(A, 4, 7);
  Randomize;
  for i := Low(A) to High(A) do
    for j := Low(A[0]) to High(A[0]) do
      A[i, j] := Math.RandomRange(1, 50);

  // Приравниваем часть элементов для дальнейшей "каскадной" сортировки
  A[1, 0] := A[0, 0];
  A[2, 0] := A[0, 0];
  A[1, 1] := A[0, 1];
  
  // Распечатаем, что получилось
  Writeln('Исходный массив:');
  PrintMatrix(A);

  // Сортируем по убыванию по 1-й колонке, конструируя компаратор
  // с помощью анонимного метода
  TArray.Sort<TIntegerDynArray>(A, TComparer<TIntegerDynArray>.Construct(
    function (const Left, Right: TIntegerDynArray): Integer
    begin
      Result := Right[0] - Left[0];
    end));
  Writeln('По убыванию в 1 столбце:');
  PrintMatrix(A);

  // Сортируем по убыванию по 1-й колонке "каскадом"
  TArray.Sort<TIntegerDynArray>(A, TComparer<TIntegerDynArray>.Construct(
    function (const Left, Right: TIntegerDynArray): Integer
    var
      i: Integer;
    begin
      i := 0;
      repeat
        Result := Right[i] - Left[i];
        Inc(i);
      until ((Result <> 0) or (i = Length(Left)));
    end));
  Writeln('Каскадная сортировка, начиная с 1-го столбца:');
  PrintMatrix(A);

  ...
  
Результат работы приведен на Рисунке 4:

Рисунок 4 - Результаты сортировки двумерного массива с использованием TArray.Sort

3.2. TDictionary<T> и TObjectDictionary<T>
Данные классы представляют из себя коллекцию пар ключ-значение, или попросту словарь. Рассмотрим работу с этими классами на примере TDictionary<T>.
3.2.1. Работа со словарем (на примере TDictionary<T>)
Основные возможности и свойства TDictionary<T> рассмотрим на следующем примере:
Листинг 15 - Использование TDictionary<T> в качестве коллекции данных для телефонной книги
...

uses
  SysUtils, Generics.Collections, Generics.Defaults;

type

  // Информация о владельце номера
  TSubscriberInfo = record
    Name, SName: string;
    class function Create(const Name, SName: string): TSubscriberInfo; static;
    function ToString: string;
  end;

class function TSubscriberInfo.Create(const Name,
  SName: string): TSubscriberInfo;
begin
  Result.Name := Name;
  Result.SName := SName;
end;

function TSubscriberInfo.ToString: string;
begin
  Result := Format('%s %s', [Name, SName]);
end;

var
  // Объявляем "словарь"
  // ключом будет номер телефона, по которому можно будет
  // определить информацию о владельце в виде TSubscriberInfo
  TelephoneDirectory: TDictionary<string, TSubscriberInfo>;
  tempInfo: TSubscriberInfo;
begin

  ...

  // Создаем справочник
  // Конструктор имеет несколько перезагруженных вариантов, позволяющих
  // установить емкость контейнера, компаратор для значений или
  // первоначальный данные - мы используем самый простой вариант
  TelephoneDirectory := TDictionary<string, TSubscriberInfo>.Create;

  // ---------------------------------------------------
  // 1) Добавление в словарь

  // Добавляем абонентов в справочник
  TelephoneDirectory.Add('9101111111', TSubscriberInfo.Create('Арнольд', 'Шварценеггер'));
  TelephoneDirectory.Add('9102222222', TSubscriberInfo.Create('Джессика', 'Альба'));
  TelephoneDirectory.Add('9103333333', TSubscriberInfo.Create('Бред', 'Питт'));
  TelephoneDirectory.Add('9104444444', TSubscriberInfo.Create('Бред', 'Питт'));
  TelephoneDirectory.Add('9105555555', TSubscriberInfo.Create('Сандра', 'Баллок'));
  // Добавляем нового абонента и заменяем, если такой номер уже есть
  TelephoneDirectory.AddOrSetValue('9104444444',
                                   TSubscriberInfo.Create('Анджелина', 'Джоли'));

  // ---------------------------------------------------
  // 2) Получение, поиск, работа с элементами

  // Имеется ли номер телефона (ключ) - ContainsKey
  if TelephoneDirectory.ContainsKey('9105555555') then
    Writeln('Номер 9105555555 зарегистрирован!');
  // Имеется ли абонент (значение) - ContainsValue
  tempInfo := TSubscriberInfo.Create('Сандра', 'Баллок');
  if TelephoneDirectory.ContainsValue(tempInfo) then
    Writeln(Format('%s есть в справочнике!', [tempInfo.ToString]));
  // Пробуем получить информацию по телефону через TryGetValue
  if TelephoneDirectory.TryGetValue('9104444444', tempInfo) then
    Writeln(Format('Номер 9104444444 у абонента %s', [tempInfo.ToString]));
  // Обращение по номеру телефона
  Writeln(Format('Абонент с номером 9101111111: %s', [TelephoneDirectory['9101111111'].ToString]));
  // Кол-во людей в справочнике
  Writeln(Format('Всего абонентов в справочнике: %d', [TelephoneDirectory.Count]));

  // ---------------------------------------------------
  // 3) Удаление элементов

  // Шварценеггера сейчас не будет в списке =)
  TelephoneDirectory.Remove('9101111111');
  // Полностью очищаем список
  TelephoneDirectory.Clear;
  
  FreeAndNil(TelephoneDirectory);

  Readln;
end.
3.2.2. События OnKeyNotify и OnValueNotify
На случай, если Вы хотите отслеживать изменения в словаре, предусмотрены события OnKeyNotify и OnValueNotify, срабатывающие при добавлении/удалении ключа и значения соответственно. Добавим небольшой фрагмент кода к предыдущему Листингу и продемонстрируем их работу:
Листинг 16 - Использование событий TDictionary<T>
...

uses
  SysUtils, Generics.Collections, Generics.Defaults;

type

...

  // Класс, содержащий обработчики добавления/удаления элементов словаря
  TDictionaryEventsHandler = class
  public
    // Синтаксис процедур един, единственное, в обоих случаях для Item необходимо
    // указать тип согласно типам нашего ключа и значения
    class procedure OnKeyNotify(Sender: TObject; const Item: string;
      Action: TCollectionNotification);
    class procedure OnValueNotify(Sender: TObject; const Item: TSubscriberInfo;
      Action: TCollectionNotification);
  end;
  
class procedure TDictionaryEventsHandler.OnKeyNotify(Sender: TObject;
  const Item: string; Action: TCollectionNotification);
begin
  case Action of
    cnAdded:
      Writeln(Format('OnKeyNotify! Номер %s добавлен!', [Item]));
    cnRemoved:
      Writeln(Format('OnKeyNotify! Номер %s удален!', [Item]));
  end;
end;

class procedure TDictionaryEventsHandler.OnValueNotify(Sender: TObject;
  const Item: TSubscriberInfo; Action: TCollectionNotification);
begin
  case Action of
    cnAdded:
      Writeln(Format('OnValueNotify! Абонент %s добавлен!', [Item.ToString]));
    cnRemoved:
      Writeln(Format('OnValueNotify! Абонент %s удален!', [Item.ToString]));
  end;
end;

...

begin

  ...

  // ---------------------------------------------------
  // 4) События добавления/удаления значений

  // События OnKeyNotify и OnValueNotify предназначены для "слежения"
  // за добавлением/удалением ключей и значений соответственно
  // Эти обработчики мы реализовали выше в классе TDictionaryEventsHandler
  TelephoneDirectory.OnKeyNotify := TDictionaryEventsHandler.OnKeyNotify;
  TelephoneDirectory.OnValueNotify := TDictionaryEventsHandler.OnValueNotify;

  Writeln;
  // Пробуем, как откликаются события
  TelephoneDirectory.Add('9101111111', TSubscriberInfo.Create('Арнольд', 'Шварценеггер'));
  TelephoneDirectory.Add('9102222222', TSubscriberInfo.Create('Джессика', 'Альба'));
  TelephoneDirectory.Clear;
  
...
end.
Результат работы приведен на Рисунке 5:

Рисунок 5 - Обработка событий OnKeyNotify и OnValueNotify в TDictionary<T>

3.2.3. Ключи, значения и TArray
Получить досутп к элементам словаря можно сделать несколькими способами:
Листинг 17 - Доступ к элементам TDictionary<T>
...

uses
  SysUtils, Generics.Collections;

...

var
  TelephoneDirectory: TDictionary<string, TSubscriberInfo>;  
  // TPair - обощенная запись, предназначенная для хранения
  // пары ключ-значение и не обладающая какой-либо другой функциональностью,
  // поэтому я не освещая ее отдельно
  TTelephoneArray: TArray<TPair<string, TSubscriberInfo>>;
  TTelephoneArrayItem: TPair<string, TSubscriberInfo>;
  PhoneNumber: string;
  Subscriber: TSubscriberInfo;
begin
  ...

  TelephoneDirectory := TDictionary<string, TSubscriberInfo>.Create;

  TelephoneDirectory.Add('9101111111', TSubscriberInfo.Create('Моника', 'Белуччи'));
  TelephoneDirectory.Add('9102222222', TSubscriberInfo.Create('Сильвестр', 'Сталлоне'));
  TelephoneDirectory.Add('9103333333', TSubscriberInfo.Create('Брюс', 'Уиллис'));

  // Показываем ключи (телефоны)
  Writeln('Ключи (телефоны):');
  for PhoneNumber in TelephoneDirectory.Keys do
    Writeln(PhoneNumber);
  Writeln;

  // Показываем значения (абонентов)
  Writeln('Значения (абоненты):');
  for Subscriber in TelephoneDirectory.Values do
    Writeln(Subscriber.ToString);
  Writeln;

  // Теперь все вместе
  Writeln('Список абонентов с телефонами:');
  for PhoneNumber in TelephoneDirectory.Keys do
    Writeln(Format('%s: %s',
      [PhoneNumber, TelephoneDirectory[PhoneNumber].ToString]));
  Writeln;

  // Кроме того, мы можем "экспортировать" словарь в знакомый нам TArray
  // Отсортируем полученный массив и выведем на экран
  TTelephoneArray := TelephoneDirectory.ToArray;
  TArray.Sort<TPair<string, TSubscriberInfo>>(
    TTelephoneArray, TComparer<TPair<string, TSubscriberInfo>>.Construct(
      function (const Left, Right: TPair<string, TSubscriberInfo>): Integer
      begin
        // Сравним сначала полные имена, а затем телефоны при необходимости
        Result := CompareStr(Left.Value.ToString, Right.Value.ToString);
        if Result = 0 then
          Result := CompareStr(Left.Key, Right.Key);
      end));
  // Печатаем
  Writeln('Отсортированный через TArray список абонентов с телефонами:');
  for TTelephoneArrayItem in TTelephoneArray do
    Writeln(Format('%s: %s',
      [TTelephoneArrayItem.Value.ToString, TTelephoneArrayItem.Key]));

  FreeAndNil(TelephoneDirectory);

  Readln;
end.
Результат работы приведен на Рисунке 6:

Рисунок 6 - Доступ к ключам и значениям TDictionary<T> и экпорт в TArray
3.3. TList<T> и TObjectList<T>
TList<T> и TObjectList<T> - упорядоченные списки.
3.3.1. Работа со списком (на примере TObjectList<T>)
Стандартные возможности по работе с классом TObjectList<T> приведены в Листинге 18:
Листинг 18 - Использование TObjectList<T> в качестве футбольного "менеджера"
...

uses
  SysUtils, DateUtils,
  Generics.Collections, Generics.Defaults;

type
  TPlayer = class
  public
    Name, Team: string;
    BirthDay: TDateTime;
    NTeamGoals: Byte; // Кол-во голов за национальную сборную
    constructor Create(const Name: string; BirthDay: TDateTime;
      const Team: string; NTeamGoals: Byte = 0);
    function ToString: string;
  end;

constructor TPlayer.Create(const Name: string; BirthDay: TDateTime;
  const Team: string; NTeamGoals: Byte);
begin
  Self.Name := Name;
  Self.BirthDay := BirthDay;
  Self.Team := Team;
  Self.NTeamGoals := NTeamGoals;
end;

function TPlayer.ToString: string;
begin
  Result := Format('%s - Возраст: %d Сборная: %s Голов: %d',
                   [Name, DateUtils.YearsBetween(Date, BirthDay),
                    Team, NTeamGoals])
end;

var
  // Объявляем TObjectList для хранения TPlayer
  PlayersList: TObjectList<TPlayer>;
  Player: TPlayer;

begin
  ...
  
  PlayersList := TObjectList<TPlayer>.Create;

  // ---------------------------------------------------
  // 1) Добавление элементов

  // "Простое" добавление в конец списка
  PlayersList.Add(
    TPlayer.Create('Роналдо', EncodeDate(1976, 09, 22), 'Бразилия', 62));
  PlayersList.Add(
    TPlayer.Create('Зинедин Зидан', EncodeDate(1972, 06, 23), 'Франция', 31));
  Writeln(Format('Индекс добавляемого игрока: %d',
          [PlayersList.Add(TPlayer.Create('Юрген Клинсманн',
                                          EncodeDate(1964, 07, 30),
                                          'Германия', 47))]));
  // Добавляем в указанную позицию
  PlayersList.Insert(0,
    TPlayer.Create('Луиш Фигу', EncodeDate(1972, 11, 4), 'Португалия', 33));
  // Добавляем несколько футболистов через InsertRange (AddRange работает аналогично)
  PlayersList.InsertRange(0,
    [TPlayer.Create('Девид Бекхэм', EncodeDate(1975, 05, 2), 'Англия', 17),
     TPlayer.Create('Алессандро Дель Пьеро', EncodeDate(1974, 11, 9), 'Италия', 27),
     TPlayer.Create('Рауль', EncodeDate(1977, 06, 27), 'Испания', 44)]);
  // Добавляем заранее созданный экземпляр класса
  Player := TPlayer.Create('Рауль', EncodeDate(1977, 06, 27), 'Испания', 44);
  PlayersList.Add(Player);


  // ---------------------------------------------------
  // 2) Доступ и проверка наличия элементов

  // Имеется ли игрок в списке - Contains
  if PlayersList.Contains(Player) then
    Writeln('Рауль есть в списке!');
  // Индекс игрока и кол-во элементов в списке
  Writeln(Format('Рауль %d-й в списке из %d футболистов.',
                 [PlayersList.IndexOf(Player) + 1, PlayersList.Count]));
  // Доступ по индексу
  Writeln(Format('1-й в списке: %s', [PlayersList[0].ToString]));
  // "Переворачиваем" элементы
  PlayersList.Reverse;
  Writeln('Элементы списка были "перевернуты"');


  // ---------------------------------------------------
  // 3) Перемещение и удаление элементов

  // Меняем игроков в списке местами
  PlayersList.Exchange(0, 1);
  // Перемещаем 1 игрока обратно
  PlayersList.Move(1, 0);

  // Удаляем элемент по индексу
  PlayersList.Delete(5);
  // Или несколько элементов (2), начиная с индекса (5)
  PlayersList.DeleteRange(5, 2);
  // Remove удаляет элемент из списка, если элемент существует
  // вернется его индекс в списке, иначе -1
  Writeln(Format('Удален %d-й игрок', [PlayersList.Remove(Player) + 1]));

  // Extract возвращает элемент, удаляя его из списка
  // В нашем случае Player будет = nil, т.к. Рауля мы уже удалили через Remove
  Player := PlayersList.Extract(Player);
  if Assigned(Player) then
    Writeln(Format('Извлечен: %s', [Player.ToString]));

  // Очищаем список полностью
  PlayersList.Clear;

  FreeAndNil(PlayersList);

  Readln;
end.
3.3.2. Поиск и сортировка
Методы сортировки и поиска полностью идентичны соответствующим методам класса TArray:
Листинг 19 - Сортировка и поиск в TObjectList<T>
...

// Функция сортировки по убыванию голов за сборную
function ComparePlayersByGoalsDecs(const Player1, Player2: TPlayer): Integer;
begin
  Result := Player2.NTeamGoals - Player1.NTeamGoals;
end;

var
...
  FoundIndex: Integer;

begin

...

  PlayersList := TObjectList<TPlayer>.Create;

  PlayersList.Add(
    TPlayer.Create('Зинедин Зидан', EncodeDate(1972, 06, 23), 'Франция', 31));
  PlayersList.Add(
    TPlayer.Create('Роналдо', EncodeDate(1976, 09, 22), 'Бразилия', 62));
  PlayersList.Add(
    TPlayer.Create('Юрген Клинсманн',  EncodeDate(1964, 07, 30), 'Германия', 47));

  // Сортируем с использованием ComparePlayersByGoalsDecs
  PlayersList.Sort(TComparer<TPlayer>.Construct(ComparePlayersByGoalsDecs));
  Writeln('Список игроков:');
  for Player in PlayersList do
    Writeln(Player.ToString);
  Writeln;

  // Найдем Роналдо в списке
  // Как и в случае с TArray BinarySearch требует, чтобы список был отсортирован
  // Ньанс - по сути, выполняющий тоже самое, что и IndexOf
  // BinarySearch обычно быстрее (первый не требует сортировки списка)
  Player := PlayersList[0];
  if PlayersList.BinarySearch(Player, FoundIndex,
    TComparer<TPlayer>.Construct(ComparePlayersByGoalsDecs)) then
    Writeln(Format('Роналдо идет отсортированном списке под №%d', [FoundIndex + 1]));

...
Результат работы приведен на Рисунке 7:

Рисунок 7 - Сортировка и поиск в TObjectList<T>

Кроме того TObjectList<T> имеет функцию ToArray, которая работает так же, как и в TDictionary<T>, поэтому не будем приводить ее еще раз.
3.3.3. Событие OnNotify
Событие OnNotify возникает при изменении содержимого списка. Приведем пример ее использования:
Листинг 20 - Событие OnNotify в TObjectList<T>
// Класс, содержащий обработчики добавления/удаления элементов списка
  TListEventsHandler = class
  public
    class procedure OnListChanged(Sender: TObject; const Item: TPlayer;
      Action: TCollectionNotification);
  end;

class procedure TListEventsHandler.OnListChanged(Sender: TObject; const Item: TPlayer;
  Action: TCollectionNotification);
var
  Mes: string;
begin
  // В отличие от TDictionary у нас добавляется Action = cnExtracted
  case Action of
    cnAdded:
      Mes := 'добавлен в список!';
    cnRemoved:
      Mes := 'удален из списка!';
    cnExtracted:
      Mes := 'извлечен из списка!';
  end;
  Writeln(Format('Футболист %s %s ', [Item.ToString, Mes]));
end;

...

begin
  ...

  PlayersList := TObjectList<TPlayer>.Create;
  PlayersList.OnNotify := TListEventsHandler.OnListChanged;

  PlayersList.Add(
    TPlayer.Create('Зинедин Зидан', EncodeDate(1972, 06, 23), 'Франция', 31));
  PlayersList.Add(
    TPlayer.Create('Рауль', EncodeDate(1977, 06, 27), 'Испания', 44));
  PlayersList.Add(
    TPlayer.Create('Роналдо', EncodeDate(1976, 09, 22), 'Бразилия', 62));

  PlayersList.Delete(1);
  Player := PlayersList.Extract(PlayersList[0]);
  PlayersList.Clear;

  FreeAndNil(PlayersList);

...
Результат работы приведен на Рисунке 8:

Рисунок 8 - Событие OnNotify в TObjectList<T>
3.4. TStack<T> и TObjectStack<T>
Стек представляет из себя обобщенную коллекцию элементов типа "последним пришел — первым вышел" (LIFO). Рассмотрим работу с ним на следующем примере:
Листинг 21 - Работа с TStack<T>
...

uses
  SysUtils, Generics.Collections;

type
  // Будем печь блины, класть их на тарелку и брать последний =)

  TPancakeType = (ptMeat, ptCherry, ptCurds);

  TPancake = record
    strict private
      const
        PANCAKE_TYPE_NAMES: array [TPancakeType] of string =
          ('c мясом', 'с вишней', 'с творогом');
    public
      var
        PancakeType: TPancakeType;
    class function Create(PancakeType: TPancakeType): TPancake; static;
    function ToString: string;
  end;

class function TPancake.Create(PancakeType: TPancakeType): TPancake;
begin
  Result.PancakeType := PancakeType;
end;

function TPancake.ToString: string;
begin
  Result := Format('Блин %s', [PANCAKE_TYPE_NAMES[PancakeType]])
end;

var
  PancakesPlate: TStack<TPancake>;
  Pancake: TPancake;

begin

...

  // "Создаем" тарелку с блинами
  PancakesPlate := TStack<TPancake>.Create;

  // Испечем несколько блинов
  // Push - помещает элементы в стек
  PancakesPlate.Push(TPancake.Create(ptMeat));
  PancakesPlate.Push(TPancake.Create(ptCherry));
  PancakesPlate.Push(TPancake.Create(ptCherry));
  PancakesPlate.Push(TPancake.Create(ptCurds));
  PancakesPlate.Push(TPancake.Create(ptMeat));

  // Съедим несколько блинов
  // Pop - извлекает элемент из стека
  Pancake := PancakesPlate.Pop;
  Writeln(Format('Съели блин (Pop): %s', [Pancake.ToString]));
  // Extract - аналогична Pop, но вызывает в OnNotify
  // c Action = cnExtracted вместо cnRemoved
  Pancake := PancakesPlate.Extract;
  Writeln(Format('Съели блин (Extract): %s', [Pancake.ToString]));

  // Какой блин лежит последним?
  // Peek - возвращает последний элемент, но не извлекает его из стека
  Writeln(Format('Последний блин: %s', [PancakesPlate.Peek.ToString]));

  // Покажем оставшиеся блины
  Writeln;
  Writeln(Format('Всего блинов: %d', [PancakesPlate.Count]));
  for Pancake in PancakesPlate do
    Writeln(Pancake.ToString);

  // Доедаем все
  // Clear - очищает стек
  PancakesPlate.Clear;

  FreeAndNil(PancakesPlate);

...
Результат работы приведен на Рисунке 9:

Рисунок 9 - Работа с TStack<T>

Событие OnNotify и функция ToArray работают также, как и в TList<T>.
3.5. TQueue<T> и TObjectQueue<T>
Очередь представляет из себя обобщенную коллекцию элементов, которая обслуживается по принципу "первым поступил — первым обслужен" (FIFO). Рассмотрим работу с очередью на следующем примере:
Листинг 22 - Работа с TQueue<T>
...

uses
  SysUtils, Generics.Collections;

type
  // Будем обслуживать клиентов, стоящих за новой Delphi XE2 =)

  TDelphiVersion = (dvStarter, dvProfessional, dvEnterprise, dvArchitect);

  TCustomer = record
    strict private
      const
        DV_NAMES: array [TDelphiVersion] of string =
          ('Starter', 'Professional', 'Enterprise', 'Architect');
    public
      var
        DelphiVersion: TDelphiVersion;
    class function Create(DelphiVersion: TDelphiVersion): TCustomer; static;
    function ToString: string;
  end;

class function TCustomer.Create(DelphiVersion: TDelphiVersion): TCustomer;
begin
  Result.DelphiVersion := DelphiVersion;
end;

function TCustomer.ToString: string;
begin
  Result := Format('Delphi XE2 %s', [DV_NAMES[DelphiVersion]])
end;

var
  CustomerQueue: TQueue<TCustomer>;
  Customer: TCustomer;

begin

...

  // "Создаем" очередь в пункте продажи
  CustomerQueue := TQueue<TCustomer>.Create;

  // Добавим несколько человек в очередь
  // Enqueue - помещает элемент в очередь
  CustomerQueue.Enqueue(TCustomer.Create(dvStarter));
  CustomerQueue.Enqueue(TCustomer.Create(dvProfessional));
  CustomerQueue.Enqueue(TCustomer.Create(dvProfessional));
  CustomerQueue.Enqueue(TCustomer.Create(dvProfessional));
  CustomerQueue.Enqueue(TCustomer.Create(dvEnterprise));
  CustomerQueue.Enqueue(TCustomer.Create(dvEnterprise));
  CustomerQueue.Enqueue(TCustomer.Create(dvArchitect));
  CustomerQueue.Enqueue(TCustomer.Create(dvArchitect));

  // Обслужим часть покупателей
  // Dequeue - извлекает элемент из очереди
  Customer := CustomerQueue.Dequeue;
  Writeln(Format('Продали (Dequeue): %s', [Customer.ToString]));
  // Extract - аналогична Dequeue, но вызывает в OnNotify
  // c Action = cnExtracted вместо cnRemoved
  Customer := CustomerQueue.Extract;
  Writeln(Format('Продали (Extract): %s', [Customer.ToString]));

  // За чем пришел следующий покупатель?
  // Peek - возвращает первый элемент, но не извлекает его из очереди
  Writeln(Format('Обслуживаемый покупатель пришел за %s',
                 [CustomerQueue.Peek.ToString]));

  // Оставшиеся покупатели
  Writeln;
  Writeln(Format('Всего покупателей осталось: %d', [CustomerQueue.Count]));
  for Customer in CustomerQueue do
    Writeln(Customer.ToString);

  // Обслуживаем всех
  // Clear - очищает очередь
  CustomerQueue.Clear;

  FreeAndNil(CustomerQueue);

...
Результат работы приведен на Рисунке 10:

Рисунок 10 - Работа с TQueue<T>

Событие OnNotify и функция ToArray работают также, как и в TList<T>.

4. Заключение
Мы рассмотрели 2 основных модуля, предоставляющих классы для работы с дженериками, а также рассмотрели принципы и примеры работы с ними. Исходники проектов Вы сможете скачать в следующей Части.

[Часть 1 - Введение в дженерики] [Часть 2 - Системные классы] [Часть 3 - Приложение]
[Содержание]

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

  1. хорошая статья. Можно было бы по глубже теоретическую составляющую копнуть

    ОтветитьУдалить
  2. Как только язык повернулся вякнуть "компаратор", Ты бы еще "вжопутрахатор" написал.
    Что уж говорить, если Винительный от Именительного падежа не сможет отличить. Предположительно афтар бы написал "номинативный" и "акусативный" соответственно.
    Вот это:"А как же сранивать нестандартные типы или собственные классы?" можно было бы написать: "А как же КОМПАРИРОВАТЬ нестандартные типы или собственные классы?" .
    Учи родной язык, лапоть.

    ОтветитьУдалить
    Ответы
    1. Товарищ/господин Анонимный.

      1. Во-первых, слово "компаратор" существует (см. например, http://msdn.microsoft.com/ru-ru/library/234b841s.aspx или http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BF%D0%B0%D1%80%D0%B0%D1%82%D0%BE%D1%80). Во-вторых, существует понятие "авторская редакция" (с которой Вы видимо не знакомы) - я имею право выбирать термин, который мне нравится больше и уж точно больше, чем "сравниватель". Посему претензия совершенно непонятна, если Вы, конечно, не из этих: http://www.colta.ru/docs/14293.

      2. По личным претензиям. Комментирование постов данного блога, как и любого другого, подразумевает, что автор будет выражать свои мысли лаконично, без каких-либо наездов и оскорблений в сторону автора поста, что у Вас не наблюдается в принципе.
      Учитывая все это, с Вашей стороны не прилично давать мне советы относительно изучения великого и могучего, а вот я в свою очередь советую Вам (да-да, "тыкать" в гостях еще и незнакому - тоже не комильфо) освежить свои глубочайшие познания родного языка по хорошей вузовской книжке с содержанием в названии "Культура речи", и, вернувшись, конструктивно и культурно изложить мысли, если таковые останутся.

      З.Ы. При повторении событий сообщение будет удалено без дополнительных затрат моих усилий на ответ.

      Удалить
    2. Yuri Petrov наверняка знаком с электроникой. Там слово "компаратор" весьма и весьма. А вот за "сравниватель" могут и в тыкву дать. И вообще, смотри ответ выше.

      Удалить
    3. целый год анонимус отвечал Юрию Петрову )) А статья хорошая. Спасибо

      Удалить
  3. TelephoneDirectory : TDictionary;
    ....
    TelephoneDirectory := TDictionary.Create;

    По-моему это не очень комильфо. По-моему было лучше завести TTelephoneDirectory = TDictionary. С точки зрения декларативности и самодокументируемости. Нет?

    ОтветитьУдалить
    Ответы
    1. Александр,

      возможно с этой точки - да, но ведь это обучающий пример.

      Удалить
  4. По-моему, в листинге 12 ошибка. Параметры Left, Right должны быть типа TCustomer.

    ОтветитьУдалить
  5. Здравствуйте, спасибо за статью. Сейчас программирую в Code Gear 2007, скажите Generic.Collections только в версиях от 2009?
    Что мне качать=)?

    ОтветитьУдалить
    Ответы
    1. Доброго времени суток.

      До 2009, увы, не могу ничем порадовать...

      Удалить
  6. Анонимный18 июня 2013 г., 19:17

    Добрый день, подскажите, как реализовано обращение к элементам (поиск нужгого элемента по ключу) в TDictionary: элементы перебираются как в обычном массиве, или они хранятся в виде дерева?

    ОтветитьУдалить
    Ответы
    1. Добрый день.

      TDictionary - это хеш-таблица, поэтому никакого перебора нет, доступ к элементу O(1).

      Удалить
    2. Анонимный18 июня 2013 г., 19:31

      Спасибо за оперативный ответ

      Удалить
  7. Анонимный17 июня 2016 г., 16:52

    Здравствуйте!
    Спасибо за статью. У меня просьба, если это возможно.
    Показать пример сортировки в алфавитном порядке двумерного массива,
    заполненого строками с использованием TArray.Sort.
    Спасибо и всего доброго.
    Сергей

    ОтветитьУдалить
  8. Этот комментарий был удален автором.

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

Поделитесь с друзьями!

 

Подписчики

Статистика