Social Icons

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

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

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

  1. Что такое дженерики и зачем они нужны?
  2. Преимущества использования дженериков
    1. Безопасность типов
    2. Эффективность
    3. Максимальное повторное использование кода
  3. Встроенные обощенные классы в Delphi
  4. Что "поддается обобщению" в Delphi?
    1. Обобщенные методы
    2. Обобщенные классы
    3. Обобщенные записи
  5. Заключение

1. Что такое дженерики и зачем они нужны?
Наличие обобщений в языке позволяет создавать открытые типы, которые превращаются в закрытые на этапе компиляции. Синтаксис дженериков на примере обобщенной записи TPoint<T> приведен в Листинге 1:
Листинг 1 - Объявление обобщенной записи TPoint<T>
type
  TPoint<T> = record
    X: T;
    Y: T;
  end;  
Сразу бросаются в глаза отличия от декларирования обычной записи - наличие <T> в имени записи и кооринат X и Y этого же типа T. T здесь - неуточненный тип, который будет указан позже, при создании конкретного экземпляра записи.
Предположим, что мы решили использовать в приложении "дробные" точки (например, Double). Все, что нужно сделать - объявить следующий закрытый тип:
Листинг 2 - Использование обобщенной записи TPoint<T> в качестве "дробной" точки
...

var
  MyPoint: TPoint<Double>;
begin
  MyPoint.X := 1.5;
  MyPoint.Y := -0.5;

...
А если нам понадобится целый тип, мы просто изменим Double на Integer:
Листинг 3 - Использование обобщенной записи TPoint<T> в качестве "целой" точки
...

var
  MyPoint: TPoint<Integer>;
begin
  MyPoint.X := 1;
  MyPoint.Y := 100;

...
Просто, не правда ли? MyPoint: TPoint<Double> и MyPoint: TPoint<Integer> - уже являются закрытыми типами и подчиняются все правилам, справедливым для обычных, необобщенных типов.

Может возникнуть вопрос: могу ли я сделать это без дженериков? Конечно, можете. Правда, лишитесь ряда преимуществ.

2. Преимущества использования дженериков
2.1. Безопасность типов
Когда необходимо повысить безопасность типов и избежать ошибок их несоответствия во время выполнения приложения - дженерики могут прийти на помощь. Для демонстрации сравним стандартный класс TList и его обобщенный "аналог" TList<T>. Как известно, TList хранит массив указателей на объекты, причем тип этих объектов может быть различен. Рассмотрим следующий пример:
Листинг 4 - Вызов метода класса TCustomer для элементов TList
...

type
  // TCustomer - произвольный класс "Клиент"

  TCustomer = class
  private
    FName: string;
  private
    constructor Create(const Name: string);
    function ShowName: string;
  end;

...

procedure PrintCustomersInfo(List: TList);
var
  Item: Pointer;
  Customer: TCustomer;
begin
  for Item in List do
    ShowMessage((TObject(Item) as TCustomer).ShowName);
end;

procedure PrintCustomersInfo2(List: TList);
var
  Item: Pointer;
  Customer: TCustomer;
begin
  for Item in List do
    if TObject(Item) is TCustomer then
      ShowMessage((TObject(Item) as TCustomer).ShowName);
end;
Теперь представьте, что передаваемый TList содержит не только экземпляры TCustomer. Для PrintCustomersInfo это будет катострофично и приведет к Invalid Type Cast, в процедуре PrintCustomersInfo2 мы избежали этого путем дополнительных проверок.

Но разве не замечательно бы было отдать такие проверки на откуп компилятору при сборке приложения? Дженерики позволяют это сделать:
Листинг 5 - Вывод информации о клиентах через TList<T>
procedure PrintCustomersInfo3(List: TList<TCustomer>);
var
  Customer: TCustomer;
begin
  for Customer in List do
    ShowMessage(Customer.ShowName);
end;
Заметили, что код уменьшился и стал более читаемым? Кроме того, за тем, чтобы в TList не попало ничего лишнего уже проследил компилятор.
2.2. Эффективность
Дополнительная эффективность при использовании дженериков - возможно, одно из главных их преимуществ. Обобщения предоставляют компилятору больше информации, не исключая данные о типе во время исполнения приложения. Такой код проще писать, эффективнее заниматься отладкой приложения. Кроме того, в рассматриваемом примере ассемблерный код с дженериками (PrintCustomersInfo3) содержит до 10 инструкций меньше (по сравнению с PrintCustomersInfo2).
2.3. Максимальное повторное использование кода
Обобщенный класс, код для которого был написан всего 1 раз, может использоваться многократно. Так, без переписывания кода, TList<T> может быть использован для создания списка целых чисел (TList<Integer>), строк (TList<string>) и т.д.

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

3. Встроенные обощенные классы в Delphi
"Из коробки" в Delphi уже имеется ряд стандартных обобщенных классов, которые можно использовать при написании приложений. Находятся они в модулях Generics.Defaults и Generics.Collections. Основные классы и типы данных приведены в Таблицах 1 и 2.
Таблица 1 - Некоторые классы модуля Generics.Defaults
IComparer Обобщенный интерфейс IComparer предназначен для сравнения двух значений одинакового типа
IEqualityComparer Обобщенный интерфейс IEqualityComparer используется для проверки равенства двух значений
TComparer Базовый обощенный класс для классов, реализующих интерфейс IComparer
TEqualityComparer Базовый обощенный класс для классов, реализующих интерфейс IEqualityComparer
TCustomComparer Базовый обощенный класс для классов, реализующих интерфейсы IComparer и IEqualityComparer
Таблица 2 - Некоторые классы и типы модуля Generics.Collections
Классы
TArray Класс, содержащий статические методы для поиска и сортировки обобщенного массива
TDictionary,
TObjectDictionary
Словарь (коллекция пар ключ-значение)
TList,
TObjectList
Упорядоченный список
TStack,
TObjectStack
Реализация стека (последний пришел, первый вышел)
TQueue,
TObjectQueue
Реализация очереди (первый пришел, первый вышел)
Типы
TPair Запись, хранящая пару ключ-значение
 
Примечание: как и аналоги из модуля Classes, обощенные "объектные" классы относительно "необъектных" (например, TObjectList<T> по сравнению с TList<T>) позволяют хранить объекты в качестве своих элементов, а также автоматически следить за их жизненным циклом

Использовать стандартные обобщенные классы довольно просто: включаем соответствующие модули в раздел uses и задействуем нужные нам классы. В Листинге 6 приведен пример работы со списком целых чисел на основе обобщенного класса TList<T>.
Листинг 6 - Пример использования TList<T> для создания списка целых чисел
...

uses
  Generics.Collections;

...

var
  IntegerList: TList<Integer>;
begin
  IntegerList := TList<Integer>.Create;
  try
    IntegerList.Add(1);
    IntegerList.Add(5);
    IntegerList.AddRange([2, 5, 8, 9]);
    IntegerList.Insert(0, 0);
    // Имеется ли 9 в списке = True
    ShowMessage(BoolToStr(IntegerList.Contains(9), True));
    IntegerList.Remove(9);
    // Теперь 9-ки уже нет = False
    ShowMessage(BoolToStr(IntegerList.Contains(9), True)); 
    // Индекс двойки в списке равен 2? True
    ShowMessage(BoolToStr(IntegerList.IndexOf(5) = 2, True)); 
  finally
    FreeAndNil(IntegerList);
  end;
end;
Более подробно системные классы будут рассмотрены во 2-м разделе.

4. Что "поддается обобщению" в Delphi?
Естественно, что в Delphi имеется возможность не только использовать имеющуюся библиотеку дженериков, но и создавать свои собственные. Обобщенными могут быть классы, интерфейсы и записи. Также поддерживается создание обобщенных методов (процедур и функций).
4.1. Обобщенные методы
Самым простым примером обобщенного метода может служить процедура для обмена значений переменных:
Листинг 7 - Пример дженериковой процедуры Swap<T>
...

  TSwapper = class
    class procedure Swap<T>(var a, b: T);
  end;

class procedure TSwapper.Swap<T>(var a, b: T);
var
  temp: T;
begin
  temp := b;
  b := a;
  a := temp;
end;

...
Использовать такую процедуру можно следующим образом:
Листинг 8 - Использование дженериковой процедуры Swap<T>
...

  TPoint<T> = record
    X: T;
    Y: T;   
    // Добавим следующую маленькую функцию,
    // чтобы легче производить инициализацию записи
    class function Create(X, Y: T): TPoint<T>; static;
  end;

class function TPoint<T>.Create(X, Y: T): TPoint<T>;
begin
  Result.X := X;
  Result.Y := Y;
end;

var
  a, b: Integer;
  s1, s2: string;
  p1, p2: TPoint<Double>;
begin
  // Теперь с помощью обобщенной процедуры
  // мы имеем возможность обменять значения целых чисел...
  a := 1;
  b := 10;
  Writeln(Format('До обмена: a=%d; b=%d', [a, b]));
  TSwapper.Swap<Integer>(a, b);
  Writeln(Format('После обмена: a=%d; b=%d', [a, b]));

  Writeln;

  // ... строк...
  s1 := 'Delphi';
  s2 := 'XE';
  Writeln(Format('До обмена: s1=%s; s2=%s', [s1, s2]));
  TSwapper.Swap<string>(s1, s2);
  Writeln(Format('После обмена: s1=%s; s2=%s', [s1, s2]));

  Writeln;
  
  // ... и даже нашего типа TPoint<T>!
  p1 := TPoint<Double>.Create(5.5, -10);
  p2 := TPoint<Double>.Create(-1.5, 10);
  Writeln(Format('До обмена: p1: (%.2f, %.2f); p2: (%.2f, %.2f);', [p1.X, p1.Y, p2.X, p2.Y]));
  TSwapper.Swap<TPoint<Double>>(p1, p2);
  Writeln(Format('После обмена: p1: (%.2f, %.2f); p2: (%.2f, %.2f);', [p1.X, p1.Y, p2.X, p2.Y]));

...

Результат приведен на Рисунке 1:

Рисунок 1 - Пример использования Swap<T>

4.2. Обобщенные классы
Приведем пример обобщенного класса массива:
Листинг 9 - Пример обобщенного класса массива TGenericArray<T>
...

type
  // Обобщенный класс - одномерный массив
  TGenericArray<T> = class
  private
    // Обобщенные члены класса
    FItems: array of T;
    function GetItem(Index: Integer): T;
    function GetCount: Integer;
    procedure SetItem(Index: Integer; Value: T);
  public
    // Обобщенный конструктор
    constructor Create;
    // Обобщенные методы
    function IndexOf(Value: T): Integer;
    function SetSize(NewSize: Integer): Boolean;
    // Обобщенные свойства
    property Items[I: Integer]: T read GetItem write SetItem; default;
    property Count: Integer read GetCount;
  end;


{ TGenericArray<T> }

constructor TGenericArray<T>.Create;
begin
  SetSize(0);
end;

function TGenericArray<T>.GetCount: Integer;
begin
  Result := Length(FItems);
end;

function TGenericArray<T>.GetItem(Index: Integer): T;
begin
  if (Index < 0) or (Index >= Length(FItems)) then
    raise EArgumentOutOfRangeException.Create(SArgumentOutOfRange);
  Result := FItems[Index];
end;

function TGenericArray<T>.IndexOf(Value: T): Integer;
begin
  // здесь может быть поиск элемента
end;

procedure TGenericArray<T>.SetItem(Index: Integer; Value: T);
begin
  if (Index < 0) or (Index >= Length(FItems)) then
    raise EArgumentOutOfRangeException.Create(SArgumentOutOfRange);
  FItems[Index] := Value;
end;

function TGenericArray<T>.SetSize(NewSize: Integer): Boolean;
begin
  Result := NewSize > 0;
  if Result then
    SetLength(FItems, NewSize);
end;

...
Посмотрим на вариант его использования:
Листинг 10 - Использование обобщенного класса массива TGenericArray<T>
...

var
  IntArray: TGenericArray<Integer>;
  DoubleArray: TGenericArray<Double>;
  i: Integer;

begin
  // Создаем "целочисленный вариант" нашего класса-массива
  IntArray := TGenericArray<Integer>.Create;
  IntArray.SetSize(3);
  IntArray[0] := 1;
  IntArray[1] := 2;
  IntArray[2] := 3;

  // а теперь "дробный"
  DoubleArray := TGenericArray<Double>.Create;
  DoubleArray.SetSize(4);
  DoubleArray[0] := 5;
  DoubleArray[1] := 2.5;
  DoubleArray[2] := -3;
  DoubleArray[3] := 3;

  Writeln(Format('В IntArray %d элем.:', [IntArray.Count]));
  for i := 0 to IntArray.Count - 1 do
    Writeln(Format('%d-й элемент = %d', [i, IntArray[i]]));

  Writeln;

  Writeln(Format('В DoubleArray %d элем.:', [DoubleArray.Count]));
  for i := 0 to DoubleArray.Count - 1 do
    Writeln(Format('%d-й элемент = %.1f', [i, DoubleArray[i]]));

  FreeAndNil(IntArray);
  FreeAndNil(DoubleArray);

...
Результат приведен на Рисунке 2.

Рисунок 2 - Пример использования TGenericArray<T>
4.3. Обобщенные записи
Пример обобщенной записи TPoint<T> уже был приведен в начале раздела. Гляньте на нее еще разок.

5. Заключение
Мы познакомились с синтаксисом дженериков, их преимуществами и возможностями в Delphi. В следующем разделе мы рассмотрим системные обобщенные классы из модулей Generics.Defaults и Generics.Collections.

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

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

  1. Отличная статья, спасибо! Пошел применять дженерики в своем проекте

    ОтветитьУдалить
  2. Хочу свой TIntegerList во всем проекте заменить на TList, однако в моем TIntegerList есть свойство AcceptDuplicates (защита от дубликатов при добавлении элементов в список). Как наиболее изящно реализовать это свойство в TList?

    ОтветитьУдалить
  3. >> Отличная статья

    Спасибо!


    >>Хочу свой TIntegerList во всем проекте заменить на TList...

    Как вариант, думаю что подойдет создать класс на основе TList и добавить туда это свойство. При этом переопределить Add и проверять в нем, есть ли уже такой элемент.

    ОтветитьУдалить
  4. ... использован для создания списка дробных чисел (TList<Integer>)...


    По моему тут опечатка, спасибо статья хоршая.

    ОтветитьУдалить
  5. Спасибо!

    По-моему опечатки там нет )

    ОтветитьУдалить
  6. а я всегда думал что "дробные числа" <> Integer.

    ОтветитьУдалить
  7. Вдруг кому будет интересен подход из каменного века:
    http://18delphi.blogspot.com/2013/03/generic-generic.html

    ОтветитьУдалить
  8. в заголовке 4. Что "поддается обощению" в Delphi? опечатка в слове "обобщению"

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

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

 

Подписчики

Статистика