Generics/ru
Contents
Вступление
Free Generics Library или FGL является нативной реализацией class templates, написанной в обобщенном синтаксисе Objfpc.
Rtl-generics package является реализацией class templates, написанной в синтаксисе дженериков Delphi, и пытается быть совместимым с библиотекой дженериков Delphi. Этот пакет является стандартным в FPC 3.1.1.+, но есть версия, доступная для FPC 3.0.4.
Обе [библиотеки], FGL и rtl-generics могут использоваться в обоих режимах.
Модуль fgl
Как начать
В следующем простом примере показано, как хранить несколько экземпляров определенного пользователем класса в списке:
Пользовательские классы дженериков
Если дженерики, определенные в модуле fgl, не соответствуют вашим потребностям, вам может потребоваться определить свои собственные классы дженериков с нуля, используя базовые языковые примитивы.
Класс-дженерик определяется с помощью keyword (ключевого слова) generic перед именем класса и используется в объявлении класса:
Пример реализации класса-дженерика:
Класс-дженерик может быть просто специализирован для определенного типа с помощью ключевого слова specialize.
Другие пункты
Код в основном выглядит так, как будто тот же класс был написан как общий, но с заменой T на данный тип.
Поэтому в теории не должно быть различий в скорости между «нормальным» классом и классом-дженериком.
Примеры
Пример использования дженериков для написания функции gmax(), которая принимает максимум две еще-не-типизированные переменные. Обратите внимание, что функции имеют пространство имен по имени класса. Недостатком может быть то, что дженерики не могут быть перегружены.
Применяем дженерики в RAD Studio Delphi. Создаем библиотеку сортировки списков однотипных объектов
Сегодня будем создавать в RAD Studio Delphi библиотеку классов, реализующих сортировку списков однотипных объектов.
Цель задачи
Прикладной разработчик должен получить инструмент для создания дочерних классов, в которых можно:
Приступаем
Сортировка по возрастанию — это перестановка элементов набора данных таким образом, чтобы в результате каждый последующий элемент набора был больше предыдущего. Сортировка по убыванию — то же самое, только обход результирующего набора данных нужно начинать с конца.
Сравнение объектов
Чтобы понять, какой элемент набора данных больше другого, для базовых типов необходимо применять оператор сравнения. А как быть с объектами? Базовый модуль System.Generics.Defaults включает в себя нужный нам интерфейс и реализацию класса
В интерфейсе видим единственный метод Compare вида
Для сравнения наших объектов будем использовать свои функции сравнения, которые описывают логику правил сравнения для каждого типа объектов. Заключим такие функции в отдельный класс TAllComparison. В нем и описываются все наши функции сравнения, они имеют тот же вид
А класс TComparer(T) как раз служит для сравнения двух объектов путем вызова метода Compare.
Можно использовать сравнение по умолчанию (Default), или создать свой метод сравнения Construct, чем мы и займемся.
Для удобства описание всех объектов будем хранить в отдельном модуле AllObjects. Здесь же будем хранить описание всех 100 созданных нами объектов.
Операции с объектами
Для осуществления операций с объектами списка в Delphi уже имеется нужный нам параметризированный класс, он же дженерик, с нужными нам методами
Вообще, универсальные параметризированные типы (дженерики) появились еще в Delphi 2009, но для нашего примера я использую RAD Studio Berlin 10.1 UPD1. Если у вас что-то не компилируется, нужно будет допилить пример для своей версии Delphi.
Пишем основной класс нашей библиотеки наследник TList(T)
Основной метод нашей задачи SortBy, опишем его использование далее.
Сортировка объектов
Пишем класс TAllSort, который содержит описание всех 100 методов сортировки вида:
На входе метод получает массив данных из списка, метод сравнения объектов, начальную позицию для сортировки, количество элементов в наборе. На выходе получаем количество произведенных в наборе данных перестановок.
Для удобства, все методы сортировки будем держать в отдельном модуле SortMethods.
Демонстрация
В качестве демонстрации решения представляю 2 механизма сортировки: «Быстрая» и «Пузырьком». Сортировать будем 2 типа объектов: список двумерных векторов (упорядоченные пары) типа Integer и список со строками.
Для начала отсортируем массив строк «пузырьками»:
А теперь «быстро» отсортируем массив двумерных векторов:
Вот такой результат получился с векторами:
Generics/ru
Contents
Вступление
Free Generics Library или FGL является нативной реализацией class templates, написанной в обобщенном синтаксисе Objfpc.
Rtl-generics package является реализацией class templates, написанной в синтаксисе дженериков Delphi, и пытается быть совместимым с библиотекой дженериков Delphi. Этот пакет является стандартным в FPC 3.1.1.+, но есть версия, доступная для FPC 3.0.4.
Обе [библиотеки], FGL и rtl-generics могут использоваться в обоих режимах.
Модуль fgl
Как начать
В следующем простом примере показано, как хранить несколько экземпляров определенного пользователем класса в списке:
Пользовательские классы дженериков
Если дженерики, определенные в модуле fgl, не соответствуют вашим потребностям, вам может потребоваться определить свои собственные классы дженериков с нуля, используя базовые языковые примитивы.
Класс-дженерик определяется с помощью keyword (ключевого слова) generic перед именем класса и используется в объявлении класса:
Пример реализации класса-дженерика:
Класс-дженерик может быть просто специализирован для определенного типа с помощью ключевого слова specialize.
Другие пункты
Код в основном выглядит так, как будто тот же класс был написан как общий, но с заменой T на данный тип.
Поэтому в теории не должно быть различий в скорости между «нормальным» классом и классом-дженериком.
Примеры
Пример использования дженериков для написания функции gmax(), которая принимает максимум две еще-не-типизированные переменные. Обратите внимание, что функции имеют пространство имен по имени класса. Недостатком может быть то, что дженерики не могут быть перегружены.
Использование стандартных дженериков Delphi для работы с наборами данных
Начиная с версии 2009, в Delphi на уровне языка и компилятора появилась поддержка универсальных типов или дженериков (известных также как параметризованные типы), аналога шаблонов в C++. Вместе с этими изменениями появился юнит System.Generics.Collections, служащий для работы с массивами и группировки данных в словари, списки, стеки и очереди. Именно об этом юните и о работе с ним пойдёт здесь речь.
Статья рассчитана на читателей, имеющих представление о том, что такое универсальный тип или шаблон. Здесь я буду рассматривать только использование юнита System.Generics.Collections. Будут рассмотрены основные классы, которые в нём реализованы, и даны примеры использования. Все приведённые примеры сделаны для Delphi XE7 и их работоспособность в других версиях Delphi не гарантируется.
TArray
Обратите внимание, что универсальный тип для массива (TArray ) определён в юните System, а в юните System.Generics.Collections определён лишь вспомогательный класс, который может только сортировать массив и искать в нём.
Теперь рассмотрим вариант сортировки с использованием своего компаратора (сравнивателя). Допустим, что в примере нам нужно сортировать строки не по алфавиту, а по своему собственному алгоритму.
TDictionary и TObjectDictionary
Словарь TDictionary или TObjectDictionary – это коллекция пар ключ-значение. Разница между этими двумя классами в том, что второй класс умеет автоматически удалять экземпляры ключей-объектов и/или значений-объектов, т.е. вы можете использовать в качестве ключей или значений экземпляры объектов.
Добавить ключ с соответствующим значением в словарь вы можете с помощью методов Add (вернёт ошибку, если попытаться добавить ключ повторно) или AddOrSetValue (заменит значение для ключа, если ключ уже есть в коллекции). Удалять элементы словаря можно с помощью Remove (удаление одного элемента) и Clear (полная очистка словаря). Полезными могут быть события OnKeyNotify и OnValueNotify, которые происходят при добавлении, изменении или удалении пары (следует учитывать, что для одной операции может произойти несколько событий).
Добавление или удаление пары ключ-значение, а также чтение значения по ключу являются эффективными, близкими к O (1), т.к. для хранения пар используется хэш-таблица. Ключи не могут быть nil, а значения – могут.
Узнать наличие в словаре ключа или значения можно с помощью методов TryGetValue (пытается считать значение по ключу), ContainsKey (проверяет наличие ключа) и ContainsValue (проверяет наличие значения). Прочитать значение по ключу можно с помощью свойства Items, узнать количество пар в словаре – с помощью свойства Count. Получить список всех ключей можно из свойства Keys, а значений – из свойства Values.
Рассмотрим несколько вариантов использования словарей.
Теперь рассмотрим более сложный вариант, когда в качестве ключа используется экземпляр класса. Также определим для ключа свой компаратор (сравниватель).
TList и TObjectList
Список TList или TObjectList – это упорядоченный список, доступ к элементам которого происходит по индексу. Разница между этими классами в том, что второй класс умеет автоматически удалять экземпляры элементов при их удалении из списка.
В список можно добавлять или вставлять элементы, менять и удалять их. Можно добавлять nil. При изменении списка срабатывает событие OnNotify.
Список можно сортировать, используя стандартные или свои компараторы. Можно искать в нём и делать реверсию.
Свойство Count показывает количество элементов в списке, а Capacity – количество зарезервированных мест. Прочитать элемент по индексу можно с помощью свойства Items.
Вот пример использования объекта TList.
Использование объекта TObjectList очень похоже, поэтому я лишь покажу пример, который покажет дополнительные тонкие моменты.
TThreadList
TThreadList – это тоже список, но потокобезопасный, т.е. с ним можно смело работать сразу из нескольких потоков. На самом деле – это обёртка над классом TList. Набор методов для работы с элементами здесь очень скромный: Add (добавление элемента), Clear (очистка списка), Remove (удаление элемента) и RemoveItem (удаление элемента с указанием направления поиска). А чтобы работать со списком в полную силу (чтение всех элементов, поиск, сортировка), нужно получить доступ к списку TList, который хранится внутри TThreadList. Сделать это можно с помощью функции блокировки LockList, которая заблокирует список и вернёт указатель на список TList. После работы со списком TList, список нужно разблокировать с помощью метода UnlockList. Также здесь есть очень полезное свойство Duplicates (дубликаты), которое задаёт поведение списка при добавлении дубликатов: разрешать добавление дубликатов (dupAccept), игнорировать дубликаты, не добавляя их, (dupIgnore) или генерировать ошибку при добавлении дубликата (dupError). По умолчанию свойство Duplicates имеет значение dupIgnore.
Вот пример работы со списком TThreadList (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»).
TStack и TObjectStack
Стек может быть произвольного размера. В стек можно добавлять nil. При изменении стека срабатывает событие OnNotify. Свойство Count показывает общее количество элементов в стеке.
Пример использования стека TStack.
Использование стека TObjectStack аналогичное и рассматривать его я здесь не буду. Упомяну лишь, что здесь можно использовать метод Extract, вместо Pop, если не требуется автоматическое удаление извлекаемого элемента.
TQueue и TObjectQueue
Очередь TQueue или TObjectQueue позволяет вам добавлять элементы в конец, а вытаскивать их из начала. Т.е. из очереди элементы будут считываться в том же порядке, в котором они были туда добавлены. Разница между очередями TQueue или TObjectQueue состоит в том, что очередь TObjectQueue умеет автоматически удалять объекты при удалении элементов из очереди.
Свойство Count показывает количество элементов в очереди. При добавлении или удалении элемента вызывается событие OnNotify. В очередь можно добавлять nil.
Вот пример использования очереди TQueue.
Использование стека TObjectQueue аналогичное и рассматривать его я здесь не буду. Здесь, так же как и в классах TObjectList и TObjectStack, можно использовать метод Extract вместо метода Dequeue, если не требуется автоматическое удаление извлекаемого элемента.
TThreadedQueue
Очередь TThreadedQueue подходит, например, для реализации какого либо сервера, который принимает сообщения от клиентов в одном потоке (или нескольких потоках) и складывает их в очередь, а затем берёт эти сообщения из очереди и обрабатывает их в другом потоке (или нескольких потоках).
Вот пример использования очереди TThreadedQueue (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»):
А вот результат, который будет выведен на консоль:
Теперь давайте разберёмся, как работает этот пример. Здесь чтение из очереди умышленно делается очень медленно, раз в 2 секунды. А записывающий поток пытается записать всё сразу. У него бы и получилось записать сразу все 9 сообщений, но у нас установлено ограничение на максимальный размер очереди – всего 5 элементов. Поэтому он записывает первые пять сообщений сразу, а при попытке записать шестое сообщение зависает в ожидании, пока в очереди не освободится место. Но мы опять же специально ограничили время ожидания всего одной секундой, поэтому через секунду он перестаёт ждать и выдаёт ошибку. То же самое происходит и со следующим седьмым сообщением. А вот к моменту отправки восьмого сообщения в очереди появляется свободное место и сообщение успешно записывается. С девятым опять случается неудача, потому, что только что на свободное место было записано сообщение 8 и очередь опять заполнена, а чтение происходит ну оооочень медленно.
Если будете плотно использовать этот класс, то вам может пригодиться ещё функция DoShutDown, которая объявляет, что очередь остановлена (после вызова этой функции новые элементы в очередь не добавляются, т.е. при вызове метода PushItem ничего не происходит), и свойство ShutDown, с помощью которого вы можете проверить, остановлена очередь или нет. Здесь нужно заметить, что после остановки очереди вы всё равно сможете считать попавшие туда элементы.
И в заключении об использовании стандартных дженериков Delphi.
. можно сказать следующее. Рассмотренные в статье дженерики или универсальные типы очень сильно облегчают программирование. Ведь массивы, списки и словари используются в программировании постоянно. А при использовании дженериков вы облегчаете читабельность кода. Кроме того ошибки несоответствия добавляемых типов в такие словари или списки отлавливаются ещё на этапе компиляции. Кроме того каждый из рассмотренных классов специально оптимизирован для выполнения возложенной на него задачи. Так, что если вы ещё не используете дженерики, настала пора это сделать.
Обобщенные интерфейсы в Delphi
Первод стаьи от Malcolm Groves, «Generic Interfaces in Delphi».

В проекте используется встроенный механизм издатель-подписчик. Я захотел чтобы подписчик имел для каждого типа события отдельный метод Receive, а не отдельный метод с огромным case-выражением, выбирающим действие для каждого типа события. Также я не хотел определять интерфейс для каждого типа события. Мне был нужен дженерик интерфейс подписчика, который получает тип события, как параметр.
Однако, я понятия не имел, могу ли я определить дженерик интерфейс, не говоря уже о реализации. Даже если предположить, что я могу сделать это, сможет ли Delphi выбрать правильный метод Receive для вызова? Есть только один способ узнать…
Обратите внимание: в этом примере убраны некоторые части кода, оставлены лишь те части, которые необходимы для демонстрации дженерик интерфейсов. О других частях я расскажу в своих следующих сообщениях.
Сперва я описал несколько простых событий. Их содержание не так интересно:
TSomeEvent = class
// other stuff
end;
TSomeOtherEvent = class
// other stuff
end;
Затем, я определил дженерик интерфейс
ISubscriber = interface
procedure Receive(Event : T);
end;
Этот интерфейс должен быть реализован подписчиками для получения событий разного типа. Заметьте, тип событий записан как джененрик тип T.
Затем, подписчики должны реализовать интерфейс для каждого типа событий, которые они хотят принимать. Поскольку это дженерик интерфейс — это довольно просто:
Здесь нет описания интерфейсов ISomeEventSubscriber и ISomeOtherEventSubscriber, мы можем просто использовать ISubscriber и передать тип на месте. Для этого нужно реализовать обязательно перегруженный метод Receive.
Вышеприведенный код описывает основу задумки. Остальной код вы найдете в соответствующем тестовом проекте. Реализуем несколько интерфейсов, каждый из которых имеет строго типизированные Receive события, без определения каждого из этих интерфейсов фактически.
И это работает? С первой попытки — нет, не получилось. Независимо от того, какой тип события и через какой интерфейс я передавал, всегда выполнялся последний метод Receive.

Удалил GUID и «as» — все заработало замечательно.
В будущих сообщениях покажу другие части: издателя и Брокера событий. Интересный побочный эффект определения событий, которыми интерисуется класс, в том, что брокер событий может просто проверить интерфейсы реализованные подписчиками, чтобы узнать на какие события они подписаны.




