Использование стандартных дженериков Delphi для работы с наборами данных

Автор: Alex. Опубликовано в Программирование . просмотров: 13046

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

Начиная с версии 2009, в Delphi на уровне языка и компилятора появилась поддержка универсальных типов или дженериков (известных также как параметризованные типы), аналога шаблонов в C++. Вместе с этими изменениями появился юнит System.Generics.Collections, служащий для работы с массивами и группировки данных в словари, списки, стеки и очереди. Именно об этом юните и о работе с ним пойдёт здесь речь.

Статья рассчитана на читателей, имеющих представление о том, что такое универсальный тип или шаблон. Здесь я буду рассматривать только использование юнита System.Generics.Collections. Будут рассмотрены основные классы, которые в нём реализованы, и даны примеры использования. Все приведённые примеры сделаны для Delphi XE7 и их работоспособность в других версиях Delphi не гарантируется.

TArray

Класс TArray юнита System.Generics.Collections содержит статические методы для поиска (BinarySearch) и сортировки массива (Sort). При поиске с помощью функции BinarySearch используется бинарный поиск с использованием O(log n) алгоритма, где n - количество элементов массива. Давайте рассмотрим пример использования класса TArray.

Обратите внимание, что универсальный тип для массива (TArray<T>) определён в юните System, а в юните System.Generics.Collections определён лишь вспомогательный класс, который может только сортировать массив и искать в нём.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults, System.Generics.Collections;
 
var
   strElem: string;
   intElem: integer;
   outFoundIndex: integer;
 
   //Массив строк.
   strArray: TArray<string> = ['Раз', 'Два', 'Три', 'Четыре', 'Пять'];
   //Массив чисел.
   intArray: TArray<integer> = [5, 4, 2, 1, 3];
 
begin
   try
      WriteLn('Массив строк до сортировки.');
      for strElem in strArray do Writeln(strElem);
      //Сортируем строки в массиве по алфавиту.
      TArray.Sort<string>(strArray); //Результат будет Два, Пять, Раз, Три, Четыре.
      WriteLn('Массив строк после сортировки.');
      for strElem in strArray do Writeln(strElem);
 
      WriteLn('Массив чисел до сортировки.');
      for intElem in intArray do Writeln(intElem);
      //Сортируем строки в массиве по алфавиту.
      TArray.Sort<integer>(intArray); //Результат будет 1, 2, 3, 4, 5.
      WriteLn('Массив чисел после сортировки.');
      for intElem in intArray do Writeln(intElem);
 
      WriteLn('Ищем в массиве строк заданную строку.');
      //Ищем строку, начиная с начала.
      if TArray.BinarySearch<string>(strArray, 'Пять', outFoundIndex) then //Результат будет индекс 1.
         WriteLn('Элемент найден. Индекс найденного элемента: ' + IntToStr(outFoundIndex))
      else
         WriteLn('Элемент не найден.');
 
      WriteLn('Ищем в массиве строк заданную строку, начиная с элемента с индексом 2.');
      //Ищем строку, начиная со строки с индексом 2 и до конца,
      //для сравнения строк используем стандартный для строки компаратор.
      if TArray.BinarySearch<string>(strArray, 'Пять', outFoundIndex, TComparer<string>.Default, 2, Length(strArray) - 2) then //Результат будет "Элемент не найден".
         WriteLn('Элемент найден. Индекс найденного элемента: ' + IntToStr(outFoundIndex))
      else
         WriteLn('Элемент не найден.');
 
      WriteLn('Ищем в массиве чисел заданное число.');
      //Ищем число, начиная с начала.
      if TArray.BinarySearch<integer>(intArray, 4, outFoundIndex) then //Результат будет индекс 3.
         WriteLn('Элемент найден. Индекс найденного элемента: ' + IntToStr(outFoundIndex))
      else
         WriteLn('Элемент не найден.');
 
      WriteLn('Ищем в массиве чисел заданное число, начиная с элемента с индексом 1 и проходим только 2 элемента.');
      //Ищем число, начиная с элемента с индексом 1, проходим только два элемента,
      //для сравнения чисел используем стандартный для числа компаратор.
      if TArray.BinarySearch<integer>(intArray, 4, outFoundIndex, TComparer<integer>.Default, 1, 2) then //Результат будет "Элемент не найден".
         WriteLn('Элемент найден. Индекс найденного элемента: ' + IntToStr(outFoundIndex))
      else
         WriteLn('Элемент не найден.');
 
      ReadLn;
   except
      on E: Exception do
         Writeln(E.ClassName, ': ', E.Message);
   end;
end.

Теперь рассмотрим вариант сортировки с использованием своего компаратора (сравнивателя). Допустим, что в примере нам нужно сортировать строки не по алфавиту, а по своему собственному алгоритму.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults, System.Generics.Collections;
 
var
   strElem: string;
   сomparer: IComparer<string>;
   strArray: TArray<string> = ['Два', 'Пять', 'Четыре', 'Раз', 'Три'];
 
begin
   try
      //Создаём свой компаратор для сравнения по своему алгоритму.
      сomparer := TDelegatedComparer<String>.Create
       (
         function(const Left, Right: String): Integer
            function NumberByName(const name: string): integer;
            begin
               if name = 'Раз' then
                  Result := 1
               else if name = 'Два' then
                  Result := 2
               else if name = 'Три' then
                  Result := 3
               else if name = 'Четыре' then
                  Result := 4
               else if name = 'Пять' then
                  Result := 5
               else
                  Result := 0;
            end;
         begin
            //Получаем числа по названиям и возвращаем разницу между ними.
            Result := NumberByName(Left) - NumberByName(Right);
         end
      );
 
      WriteLn('Массив строк до сортировки.');
      for strElem in strArray do Writeln(strElem);
      //Сортируем строки, используя свой компаратор.
      TArray.Sort<string>(strArray, сomparer); //Элементы будут отсортированы не по алфавиту, а по порядку.
      WriteLn('Массив строк после сортировки.');
      for strElem in strArray do Writeln(strElem);
 
      ReadLn;
   except
      on E: Exception do
         Writeln(E.ClassName, ': ', E.Message);
   end;
end.

TDictionary и TObjectDictionary

Словарь TDictionary или TObjectDictionary – это коллекция пар ключ-значение. Разница между этими двумя классами в том, что второй класс умеет автоматически удалять экземпляры ключей-объектов и/или значений-объектов, т.е. вы можете использовать в качестве ключей или значений экземпляры объектов.

Добавить ключ с соответствующим значением в словарь вы можете с помощью методов Add (вернёт ошибку, если попытаться добавить ключ повторно) или AddOrSetValue (заменит значение для ключа, если ключ уже есть в коллекции). Удалять элементы словаря можно с помощью Remove (удаление одного элемента) и Clear (полная очистка словаря). Полезными могут быть события OnKeyNotify и OnValueNotify, которые происходят при добавлении, изменении или удалении пары (следует учитывать, что для одной операции может произойти несколько событий).

Добавление или удаление пары ключ-значение, а также чтение значения по ключу являются эффективными, близкими к O (1), т.к. для хранения пар используется хэш-таблица. Ключи не могут быть nil, а значения – могут.

Узнать наличие в словаре ключа или значения можно с помощью методов TryGetValue (пытается считать значение по ключу), ContainsKey (проверяет наличие ключа) и ContainsValue (проверяет наличие значения). Прочитать значение по ключу можно с помощью свойства Items, узнать количество пар в словаре – с помощью свойства Count. Получить список всех ключей можно из свойства Keys, а значений – из свойства Values.

Рассмотрим несколько вариантов использования словарей.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, Windows, System.Generics.Collections;
 
type
   //Класс для хранения характеристик столицы.
   TCapital = class
      //Название.
      name: string;
      //Широта.
      latitude: double;
      //Долгота.
      longitude: double;
      //Деструктор класса.
      destructor Destroy; override;
      //Конструктор класса.
      constructor Create(name: string; latitude, longitude: double);
   end;
 
   destructor TCapital.Destroy;
   begin
      //При удалении выводим на консоль информацию об удаляемом классе.
      Writeln('Удаляется объект TСapital: ' + name);
      inherited Destroy;
   end;
 
   constructor TCapital.Create(name: string; latitude, longitude: double);
   begin
      inherited Create;
      //Инициализируем переменные.
      self.name := name;
      self.latitude := latitude;
      self.longitude := longitude;
   end;
 
var
   s: string;
   key: string;
   value: string;
   capital: TCapital;
   pair1: TPair<string, string>;
   pair3: TPair<string, TCapital>;
 
   //Словарь для хранения пары string-string.
   dictionary1: TDictionary<string, string>;
   //Словарь для хранения пары string-TCapital.
   //TCapital класс и хранящиеся в словаре экземпляры этого класса нужно удалять,
   //поэтому используем TObjectDictionary, чтобы удаление происходило автоматически.
   dictionary3: TObjectDictionary<string, TCapital>;
begin
   try
 
      //Создаём словарь для хранения пары string-string.
      dictionary1 := TDictionary<string, string>.Create;
      try
         //Добавление пары.
         dictionary1.Add('Россия', 'Москва');
         //Попытка добавить пару с помощью метода Add с уже имеющимся в словаре ключом
         //вызовет ошибку EListError с сообщением 'Duplicates not allowed'.
         try
            dictionary1.Add('Россия', 'Москва1');
         except
            on e: EListError do
               Writeln(e.ClassName, ': ', e.Message);
         end;
         //Метод AddOrSetValue заменит для ключа 'Россия' значение 'Москва' на 'Москва2'.
         dictionary1.AddOrSetValue('Россия', 'Москва2');
         //А здесь метод AddOrSetValue добавит пару 'Великобритания'-'Лондон'.
         dictionary1.AddOrSetValue('Великобритания', 'Лондон');
         //Узнаем есть ли ключ в словаре с помощью метода ContainsKey и добавим значение если нет.
         if not dictionary1.ContainsKey('США') then
            dictionary1.Add('США', 'Вашингтон');
         //Узнаем количество пар в словаре.
         Writeln('Количество пар в словаре dictionary1: ', dictionary1.Count);
         //Прочитаем все ключи и выведем для них значения.
         Writeln('--Ключи и значения');
         for key in dictionary1.Keys do
            Writeln(key, ': ', dictionary1[key]);
         //Прочитаем все значения.
         Writeln('--Значения');
         for value in dictionary1.Values do
            Writeln(value);
         //Прочитаем все пары ключ-значения.
         Writeln('--Пары');
         for pair1 in dictionary1 do
            Writeln(pair1.Key, ': ', pair1.Value);
         //Очистим словарь.
         dictionary1.Clear;
      finally
         dictionary1.Free;
      end;
 
      //Cоздаём словарь для хранения пары string-TCapital.
      //При создании сообщаем словарю (флаг doOwnsValues),
      //чтобы словарь автоматически отслеживал, когда нужно удалять
      //экземпляры класса TCapital (будет вызываться функция Free).
      dictionary3 := TObjectDictionary<string, TCapital>.Create([doOwnsValues]);
      try
         //Добавление пары с помощью метода Add.
         dictionary3.Add('Россия', TCapital.Create('Москва', 55.75, 37.62));
         //Добавление с помощью метода AddOrSetValue.
         dictionary3.AddOrSetValue('Великобритания', TCapital.Create('Лондон', 51.51, -0.13));
         //Проверка есть ли ключ в словаре, и если нет, то добавляем.
         if not dictionary3.ContainsKey('США') then
            dictionary3.Add('США', TCapital.Create('Вашингтон', 38.9, -77.04));
         //Прочитаем все ключи и выведем по ним информацию.
         Writeln('--Ключи и значения');
         for key in dictionary3.Keys do
            Writeln(key, ': ', dictionary3[key].name,
               ': ', FloatToStr(dictionary3[key].latitude),
               'x', FloatToStr(dictionary3[key].longitude));
         //Прочитаем все значения.
         Writeln('--Значения');
         for capital in dictionary3.Values do
            Writeln(capital.name,
               ': ', FloatToStr(capital.latitude),
               'x', FloatToStr(capital.longitude));
         //Прочитаем все пары ключ-значения.
         Writeln('--Пары');
         for pair3 in dictionary3 do
            Writeln(pair3.Key, ': ', pair3.Value.name,
               ': ', FloatToStr(pair3.Value.latitude),
               'x', FloatToStr(pair3.Value.longitude));
         //Спросим у пользователя, для какой страны он хочет узнать столицу.
         Writeln('--Поиск страны в словаре');
         Writeln('Введите название страны: ');
         Readln(s);
         //Конвертируем полученную строку из кодировки CP1251 в кодировку консоли (в моём случае CP866).
         with TEncoding.GetEncoding(GetConsoleCP) do
         begin
            s := GetString(TEncoding.ANSI.GetBytes(s));
            Free;
         end;
         //Ищем столицу введённой страны в словаре.
         if dictionary3.ContainsKey(s) then
            Writeln('Столица введённой страны: ', dictionary3[s].name)
         else
            Writeln('Ошибка! Введённая страна не найдена в словаре!');
      finally
         //При удалении (dictionary3.Free) или очистке (dictionary3.Clear) этого словаря
         //будут удалены все хранящиеся в нём экземпляры класса TCapital.
         dictionary3.Free;
      end;
 
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

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

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults, System.Generics.Collections;
 
type
   TPosition = class
      X: integer;
      Y: integer;
   end;
 
var
   position: TPosition;
   x, y: integer;
 
   //Словарь для хранения пары TPosition-string.
   //TPosition класс и хранящиеся в словаре экземпляры этого класса нужно удалять,
   //поэтому используем TObjectDictionary, чтобы удаление происходило автоматически.
   dictionary: TObjectDictionary<TPosition, string>;
   //Компаратор для сравнения двух объектов TPosition по своему алгоритму
   //и вычисления хэш-кода для объекта TPosition тоже по своему алгоритму.
   comparer: IEqualityComparer<TPosition>;
begin
   try
      //Создаём свой компаратор, используя класс TDelegatedEqualityComparer.
      comparer := TDelegatedEqualityComparer<TPosition>.Create
      (
         //Функция сравнения TEqualityComparison<TPosition>.
         function(const Left, Right: TPosition): Boolean
         begin
            //Сравниваем координаты двух объектов TPosition.
            Result := (Left.X = Right.X) and (Left.Y = Right.Y);
         end,
         //Функция вычисления хэш-кода THasher<TPosition>.
         function(const Value: TPosition): Integer
         begin
            //Генерируем хэш-код, просто используя исключающее ИЛИ (XOR).
            Result := Value.X xor Value.Y;
         end
      );
 
      //Cоздаём словарь для хранения пары TPosition-string.
      //При создании сообщаем словарю (флаг doOwnsKeys),
      //чтобы словарь автоматически отслеживал, когда нужно удалять
      //экземпляры класса TPosition (будет вызываться функция Free).
      //Для сравнения ключей и вычисления хэш-кода используем свой компаратор.
      dictionary := TObjectDictionary<TPosition, string>.Create([doOwnsKeys], comparer);
      try
         //Заполняем словарь.
         position := TPosition.Create;
         position.X := 10;
         position.Y := 10;
         dictionary.Add(position, 'Треугольник');
         position := TPosition.Create;
         position.X := 11;
         position.Y := 12;
         dictionary.Add(position, 'Квадрат');
         position := TPosition.Create;
         position.X := 20;
         position.Y := 20;
         dictionary.Add(position, 'Ромб');
         try
            position := TPosition.Create;
            position.X := 11;
            position.Y := 12;
            //Попытка добавить пару с помощью метода Add с уже имеющимся в словаре ключом
            //вызовет ошибку EListError с сообщением 'Duplicates not allowed'.
            dictionary.Add(position, 'Многогранник');
         except
            on e: EListError do
               Writeln(e.ClassName, ': ', e.Message);
         end;
         //Выводим все ключи словаря и значения для них.
         for position in dictionary.Keys do
            WriteLn('Координата: ', position.X, 'x', position.Y, ', Фигура: ', dictionary[position]);
         //Запросим у пользователя координаты и выведем фигуру для этих координат.
         WriteLn('Введите координаты: ');
         ReadLn(x, y);
         position := TPosition.Create;
         try
            position.X := x;
            position.Y := y;
            if dictionary.ContainsKey(position) then
               WriteLn('По введённой координате ', x, 'x', y, ' расположена фигура: ', dictionary[position])
            else
               WriteLn('Ошибка! Для координаты ', x, 'x', y, ' фигура не найдена!');
         finally
            position.Free;
         end;
      finally
         //При удалении (dictionary.Free) или очистке (dictionary.Clear) этого словаря
         //будут удалены все хранящиеся в нём экземпляры класса TPosition.
         dictionary.Free;
      end;
 
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

TList и TObjectList

Список TList или TObjectList – это упорядоченный список, доступ к элементам которого происходит по индексу. Разница между этими классами в том, что второй класс умеет автоматически удалять экземпляры элементов при их удалении из списка.

В список можно добавлять или вставлять элементы, менять и удалять их. Можно добавлять nil. При изменении списка срабатывает событие OnNotify.

Список можно сортировать, используя стандартные или свои компараторы. Можно искать в нём и делать реверсию.

Свойство Count показывает количество элементов в списке, а Capacity – количество зарезервированных мест. Прочитать элемент по индексу можно с помощью свойства Items.

Вот пример использования объекта TList.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults,
   System.Generics.Collections, System.Types;
 
var
   index: integer;
   item1: integer;
   i: integer;
   contains: boolean;
   //Список целых чисел.
   list1: TList<integer>;
 
begin
   try
      //Создаём список.
      list1 := TList<integer>.Create;
      try
         //Добавление одного элемента в список.
         //В результате в списке будет только число 5.
         list1.Add(5);
         //Добавляем сразу несколько элементов.
         //Элементы добавятся в конец в том же порядке.
         //В результате в списке будет 5, 2, 6, 8, 1, 9, 0.
         list1.AddRange([2, 6, 8, 1, 9, 0]);
         //Вставка в список вместо элемента с индексом 2.
         //Результат будет 5, 2, 4, 6, 8, 1, 9, 0.
         list1.Insert(2, 4);
         //Вставка нескольких элементов с индекса 1.
         //Результат будет 5, 7, 3, 4, 2, 4, 6, 8, 1, 9, 0.
         list1.InsertRange(1, [7, 3, 4]);
         //Передвигаем элемент с индексом 7 в конец списка.
         //Результат будет 5, 7, 3, 4, 2, 4, 6, 1, 9, 0, 8.
         list1.Move(7, list1.Count - 1);
         //Читаем все элементы из списка.
         for item1 in list1 do
            WriteLn(item1);
         //Линейный поиск в списке, начиная с начала.
         //Результат поиска будет 3.
         index := list1.IndexOf(4);
         //Линейный поиск в списке, начиная с конца.
         //Результат поиска будет 5.
         index := list1.LastIndexOf(4);
         //Этот метод может искать как с начала, так и с конца.
         //Направление задаётся вторым параметром.
         //Результат поиска будет 5.
         index := list1.IndexOfItem(4, TDirection.FromEnd);
         //Проверка, есть ли элемент в списке. Для проверки используется IndexOf.
         //Результат проверки будет True.
         contains := list1.Contains(4);
         //Сортировка. Результат будет 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9.
         list1.Sort;
         //Читаем элементы из списка.
         for i := 0 to list1.Count - 1 do
            WriteLn(list1[i]);
         //Поиск в отсортированном списке.
         //BinarySearch работает намного быстрее IndexOf, но только с отсортированным списком.
         //Результат поиска будет 4.
         list1.BinarySearch(4, index);
         //Изменение направления списка. Все элементы будут установлены в обратном порядке.
         //Результат будет 9, 8, 7, 6, 5, 4, 4, 3, 2, 1, 0.
         list1.Reverse;
         //Удаляем число 4 из списка.
         //В результате будет удалён только первый найденный элемент.
         //В примере функция найдёт элемент с индексом 5 и удалит его.
         //В функции используется линейный поиск с начала списка.
         //Результат будет 9, 8, 7, 6, 5, 4, 3, 2, 1, 0.
         //В переменной index будет 5, т.е. индекс удалённого элемента до его удаления.
         index := list1.Remove(4);
         //Здесь метод Remove не найдёт число 11 в списке и вернёт индекс -1.
         index := list1.Remove(11);
         //Удаляем число 4 из списка, при этом ищем с конца.
         //Результат будет 9, 8, 7, 6, 5, 3, 2, 1, 0.
         //В переменной index будет 5.
         index := list1.RemoveItem(4, TDirection.FromEnd);
         //Удаляем элемент по индексу.
         //Результат будет 9, 8, 6, 5, 3, 2, 1, 0.
         list1.Delete(2);
         //Удаляем два элемента, начиная с индекса 3.
         //Результат будет 9, 8, 6, 2, 1, 0.
         list1.DeleteRange(3, 2);
         //Удаляем из списка все элементы, значения которых являются значениями по умолчанию.
         //Для типа Integer - это 0. Следовательно, все нули будут удалены.
         //Результат будет 9, 8, 6, 2, 1.
         list1.Pack;
         //А теперь будем определять, нужно ли удалять элемент с помощью своей функции.
         //Параметр функции L - это значение элемента списка, а R - это значение по умолчанию (в нашем случае 0).
         //Удалим все элементы, значения которых больше 7.
         //Результат будет 6, 2, 1.
         list1.Pack(function(const L, R: integer): boolean
            begin
               Result := L > 7;
            end
         );
         //Меняем местами два элемента списка.
         //Результат будет 1, 2, 6.
         list1.Exchange(0, 2);
         //Полная очистка списка.
         list1.Clear;
      finally
         list1.Free;
      end;
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

Использование объекта TObjectList очень похоже, поэтому я лишь покажу пример, который покажет дополнительные тонкие моменты.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults,
   System.Generics.Collections, System.Types;
 
type
   TMyObject = class
   private
      FName: string;
   public
      constructor Create(const AName: String);
      destructor Destroy(); override;
   end;
 
var
   myObject: TMyObject;
   //Список объектов TStrings.
   list2: TObjectList<TMyObject>;
 
   constructor TMyObject.Create(const AName: String);
   begin
      FName := AName;
   end;
 
   destructor TMyObject.Destroy;
   begin
      //Показываем, когда объект удаляется.
      WriteLn('Объект "', FName, '" удалён!');
      inherited;
   end;
 
begin
   try
      //Создаём список. По умолчанию свойство OwnsObjects выставляется в True.
      //Это значит, что список сам удаляет объекты.
      list2 := TObjectList<TMyObject>.Create;
      try
         //Добавление объекта в список.
         list2.Add(TMyObject.Create('Один'));
         //Добавление нескольких объектов сразу.
         list2.AddRange([TMyObject.Create('Два'), TMyObject.Create('Три')]);
         //Удаление объекта из списка влечёт удаление объекта 'Один'.
         list2.Delete(0);
         //Изымаем объект из списка без удаления.
         //Т.к. мы извлекли объект 'Два' из списка, он не будет автоматически удалён
         //и его нужно удалять самостоятельно, но в примере мы умышленно этого не будем делать.
         myObject := list2.Extract(list2[0]);
         WriteLn('Объект "', myObject.FName, '" изъят из списка!');
         //При попытке извлечь из списка объект, которого в списке нет,
         //метод Extract вернёт значение по умолчанию. Для класса TMyClass - это nil.
         myObject := list2.Extract(myObject);
      finally
         //При удалении списка оставшиеся в нём объекты будут удалены.
         //В примере объект 'Три' будет автоматически удалён.
         list2.Free;
      end;
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

TThreadList

TThreadList – это тоже список, но потокобезопасный, т.е. с ним можно смело работать сразу из нескольких потоков. На самом деле – это обёртка над классом TList. Набор методов для работы с элементами здесь очень скромный: Add (добавление элемента), Clear (очистка списка), Remove (удаление элемента) и RemoveItem (удаление элемента с указанием направления поиска). А чтобы работать со списком в полную силу (чтение всех элементов, поиск, сортировка), нужно получить доступ к списку TList, который хранится внутри TThreadList. Сделать это можно с помощью функции блокировки LockList, которая заблокирует список и вернёт указатель на список TList. После работы со списком TList, список нужно разблокировать с помощью метода UnlockList. Также здесь есть очень полезное свойство Duplicates (дубликаты), которое задаёт поведение списка при добавлении дубликатов: разрешать добавление дубликатов (dupAccept), игнорировать дубликаты, не добавляя их, (dupIgnore) или генерировать ошибку при добавлении дубликата (dupError). По умолчанию свойство Duplicates имеет значение dupIgnore.

Вот пример работы со списком TThreadList (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»).

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Collections, System.Threading;
var
   list: TThreadList<integer>;
   writer, reader: ITask;
begin
   try
      list := TThreadList<integer>.Create;
      try
         //Создаём задачу, которая будет добавлять элементы в список.
         writer := TTask.Create(procedure()
            var
               i: integer;
            begin
               //Добавляем в список 9 элементов раз в 2 секунды.
               for i := 1 to 9 do
               begin
                  //Добавляем в список один элемент.
                  list.Add(Random(100));
                  //Ждём 2 секунды.
                  Sleep(2000);
               end;
            end
         );
         //Создаём задачу, которая будет читать элементы из списка.
         reader := TTask.Create(procedure()
            var
               listCount: integer;
               item: integer;
               internalList: TList<integer>;
            begin
               //Читаем список раз в секунду, пока количество элементов не будет равно 9.
               repeat
                  //Ждём секунду.
                  Sleep(1000);
                  //Блокируем список и одновременно получаем указатель на внутренний список, который хранит элементы.
                  internalList := list.LockList;
                  try
                     //Узнаём количество элементов в списке.
                     listCount := internalList.Count;
                     WriteLn('Количество элементов в списке: ', listCount);
                     //Читаем все элементы списка.
                     Write('Элементы списка: ');
                     for item in internalList do
                        Write(item, '; ');
                     WriteLn;
                  finally
                     //Разблокируем список.
                     list.UnlockList;
                  end;
               until listCount = 9;
            end
         );
         //Запускаем задачи.
         writer.Start;
         reader.Start;
         //Ждём пока задачи выполнятся.
         TTask.WaitForAll([writer, reader]);
      finally
         list.Free;
      end;
      ReadLn;
   except
      on E: Exception do
         Writeln(E.ClassName, ': ', E.Message);
   end;
end.

TStack и TObjectStack

Стек TStack или TObjectStack – это стек элементов, работающий по принципу «последним пришёл — первым вышел» (last in - first out). Т.е. добавленные в стек элементы, вытаскиваются из него в обратном порядке. Стеки TStack и TObjectStack отличаются друг от друга тем, что второй стек предоставляет механизм автоматического удаления объектов удаляемых из стека.

Стек может быть произвольного размера. В стек можно добавлять nil. При изменении стека срабатывает событие OnNotify. Свойство Count показывает общее количество элементов в стеке.

Пример использования стека TStack.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults,
   System.Generics.Collections, System.Types;
 
var
   item: string;
   //Стек строк.
   stack: TStack<string>;
 
begin
   try
      //Создаём стек.
      stack := TStack<string>.Create;
      try
         //Добавляем 5 элементов.
         stack.Push('Алексей');
         stack.Push('Людмила');
         stack.Push('Сергей');
         stack.Push('Наталья');
         stack.Push('Александр');
         //узнаём количество элементов в стеке.
         //Результат будет - 5 элементов.
         WriteLn('Стек содержит ' + IntToStr(stack.Count) + ' элементов.');
         //Читаем все элементы в стеке по порядку, т.е. от 'Алексей' до 'Александр'.
         //Стек при этом не меняется.
         for item in stack do
            WriteLn(item);
         //Смотрим последний добавленный элемент без изменения стека.
         //Если стек пуст, то метод Peek сгенерирует ошибку.
         //Результат будет 'Александр'.
         WriteLn(stack.Peek);
         //Извлекаем последний добавленный элемент из стека.
         //Если стек пуст, то метод Pop сгенерирует ошибку.
         //Результат будет 'Александр', при этом элемент 'Александр' будет удалён из стека.
         WriteLn(stack.Pop);
         //Извлекаем последний добавленный элемент из стека.
         //Теперь результат будет 'Наталья', при этом элемент 'Наталья' будет удалён из стека.
         WriteLn(stack.Pop);
         //Очистка стека, т.е. удаление всех элементов.
         stack.Clear;
      finally
         stack.Free;
      end;
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

Использование стека TObjectStack аналогичное и рассматривать его я здесь не буду. Упомяну лишь, что здесь можно использовать метод Extract, вместо Pop, если не требуется автоматическое удаление извлекаемого элемента.

TQueue и TObjectQueue

Очередь TQueue или TObjectQueue позволяет вам добавлять элементы в конец, а вытаскивать их из начала. Т.е. из очереди элементы будут считываться в том же порядке, в котором они были туда добавлены. Разница между очередями TQueue или TObjectQueue состоит в том, что очередь TObjectQueue умеет автоматически удалять объекты при удалении элементов из очереди.

Свойство Count показывает количество элементов в очереди. При добавлении или удалении элемента вызывается событие OnNotify. В очередь можно добавлять nil.

Вот пример использования очереди TQueue.

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults,
   System.Generics.Collections, System.Types;
 
var
   item: string;
   //Очередь строк.
   queue: TQueue<string>;
 
begin
   try
      //Создаём очередь.
      queue := TQueue<string>.Create;
      try
         //Добавляем 5 элементов.
         queue.Enqueue('Алексей');
         queue.Enqueue('Людмила');
         queue.Enqueue('Сергей');
         queue.Enqueue('Наталья');
         queue.Enqueue('Александр');
         //Узнаём количество элементов в очереди.
         //Результат будет - 5 элементов.
         WriteLn('Очередь содержит ' + IntToStr(queue.Count) + ' элементов.');
         //Читаем все элементы в очереди по порядку, т.е. от 'Алексей' до 'Александр'.
         //Очередь при этом не меняется.
         for item in queue do
            WriteLn(item);
         //Смотрим первый добавленный элемент без изменения очереди.
         //Если очередь пуста, то метод Peek сгенерирует ошибку.
         //Результат будет 'Алексей'.
         WriteLn(queue.Peek);
         //Извлекаем первый добавленный элемент из очереди.
         //Если очередь пуста, то метод Dequeue сгенерирует ошибку.
         //Результат будет 'Алексей', при этом элемент 'Алексей' будет удалён из очереди.
         WriteLn(queue.Dequeue);
         //Извлекаем первый добавленный элемент из очереди.
         //Теперь результат будет 'Людмила', при этом элемент 'Людмила' будет удалён из очереди.
         WriteLn(queue.Dequeue);
         //Очистка очереди, т.е. удаление всех элементов.
         queue.Clear;
      finally
         queue.Free;
      end;
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

Использование стека TObjectQueue аналогичное и рассматривать его я здесь не буду. Здесь, так же как и в классах TObjectList и TObjectStack, можно использовать метод Extract вместо метода Dequeue, если не требуется автоматическое удаление извлекаемого элемента.

TThreadedQueue

TThreadedQueue - это ещё одна реализация очереди, но в отличие от TQueue или TObjectQueue, эта очередь предназначена для вставки и изъятия элементов из разных потоков. Для этой очереди задаётся ограничение на максимальное количество находящихся в ней элементов, и, если очередь максимально заполнена и какой либо поток пытается добавить ещё один элемент, то этот поток ожидает, пока в очереди появится свободное место или пока не истечёт время ожидания.

Очередь TThreadedQueue подходит, например, для реализации какого либо сервера, который принимает сообщения от клиентов в одном потоке (или нескольких потоках) и складывает их в очередь, а затем берёт эти сообщения из очереди и обрабатывает их в другом потоке (или нескольких потоках).

Вот пример использования очереди TThreadedQueue (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»):

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses
   System.SysUtils, System.Generics.Defaults,
   System.Generics.Collections, System.Types, System.Threading;
 
var
   //Очередь для хранения строк.
   queue: TThreadedQueue<string>;
   //Потоки для записи в очередь и чтения из неё.
   writer, reader: ITask;
 
begin
   try
      //Создаём очередь. Здесь специально делаем размер очереди (всего 5 элементов)
      //и время ожидания на постановку в очередь (всего 1 сек.) очень маленькими.
      queue := TThreadedQueue<string>.Create(5, 1000);
      try
         //Создаём задачу, которая будет писать сообщения в поток.
         writer := TTask.Create(procedure()
            var
               i: integer;
               message: string;
               waitResult: TWaitResult;
            begin
               for i := 1 to 9 do
               begin
                  //Формируем сообщение.
                  message := Format('Сообщение %d. Создано в %s.',
                     [i, FormatDateTime('hh:nn:ss.zzz', Time)]);
                  //Пишем сообщение в очередь.
                  waitResult := queue.PushItem(message);
                  //Если превышено разрешённое время ожидания, то выдаём сообщение об этом.
                  if waitResult = wrTimeout then
                     WriteLn(Format('ОШИБКА! Не удалось отправить сообщение %d. Истекло время ожидания.', [i]));
               end;
            end
         );
         //Создаём задачу, которая будет читать сообщения из потока.
         reader := TTask.Create(procedure()
            var
               message: string;
            begin
               //Читаем сообщения из очереди, пока они там есть.
               repeat
                  //Читаем первое сообщение в очереди и одновременно вынимаем его оттуда.
                  message := queue.PopItem;
                  //Выдаём сообщение на консоль, заодно отображаем время его получения.
                  WriteLn(message, ' Получено в ' + FormatDateTime('hh:nn:ss.zzz', Time) + '.');
                  //Ждём 2 секунды (как будто сообщение очень долго обрабатывается).
                  Sleep(2000);
               until queue.QueueSize = 0;
            end
         );
         //Запускаем писателя.
         writer.Start;
         //Ждём секунду.
         Sleep(1000);
         //Запускаем читателя.
         reader.Start;
         //Ждём пока обе задачи отработают.
         TTask.WaitForAll([writer, reader]);
         //Выдаём статистические данные:)
         WriteLn('Всего сообщений отправлено: ', queue.TotalItemsPushed);
         WriteLn('Всего сообщений получено: ', queue.TotalItemsPopped);
      finally
         queue.Free;
      end;
      ReadLn;
   except
      on e: Exception do
         Writeln(e.ClassName, ': ', e.Message);
   end;
end.

А вот результат, который будет выведен на консоль:

ОШИБКА! Не удалось отправить сообщение 6. Истекло время ожидания.
ОШИБКА! Не удалось отправить сообщение 7. Истекло время ожидания.
Сообщение 1. Создано в 19:40:57.624. Получено в 19:40:59.649.
ОШИБКА! Не удалось отправить сообщение 9. Истекло время ожидания.
Сообщение 2. Создано в 19:40:57.624. Получено в 19:41:01.651.
Сообщение 3. Создано в 19:40:57.624. Получено в 19:41:03.651.
Сообщение 4. Создано в 19:40:57.624. Получено в 19:41:05.652.
Сообщение 5. Создано в 19:40:57.624. Получено в 19:41:07.652.
Сообщение 8. Создано в 19:40:59.642. Получено в 19:41:09.652.
Всего сообщений отправлено: 6
Всего сообщений получено: 6

Теперь давайте разберёмся, как работает этот пример. Здесь чтение из очереди умышленно делается очень медленно, раз в 2 секунды. А записывающий поток пытается записать всё сразу. У него бы и получилось записать сразу все 9 сообщений, но у нас установлено ограничение на максимальный размер очереди – всего 5 элементов. Поэтому он записывает первые пять сообщений сразу, а при попытке записать шестое сообщение зависает в ожидании, пока в очереди не освободится место. Но мы опять же специально ограничили время ожидания всего одной секундой, поэтому через секунду он перестаёт ждать и выдаёт ошибку. То же самое происходит и со следующим седьмым сообщением. А вот к моменту отправки восьмого сообщения в очереди появляется свободное место и сообщение успешно записывается. С девятым опять случается неудача, потому, что только что на свободное место было записано сообщение 8 и очередь опять заполнена, а чтение происходит ну оооочень медленно...

Если будете плотно использовать этот класс, то вам может пригодиться ещё функция DoShutDown, которая объявляет, что очередь остановлена (после вызова этой функции новые элементы в очередь не добавляются, т.е. при вызове метода PushItem ничего не происходит), и свойство ShutDown, с помощью которого вы можете проверить, остановлена очередь или нет. Здесь нужно заметить, что после остановки очереди вы всё равно сможете считать попавшие туда элементы.

И в заключении об использовании стандартных дженериков Delphi...

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

Tags: Параллельное программирование Delphi Учебники по программированию

Добавить комментарий


Защитный код
Обновить