WWW.BOOK.LIB-I.RU
БЕСПЛАТНАЯ  ИНТЕРНЕТ  БИБЛИОТЕКА - Электронные ресурсы
 

«Оригинал учебника: на французском – sjrd.developpez.com/delphi/tutoriel/generiques на английском – sjrd.developpez.com/delphi/tutoriel/generics Перевод: Алексей Тимохин – ...»

Sbastien Doeraene

Generics with Delphi 2009

Се ст н ур н

О о щённое прогр ммиров ние

(Generics) в Delphi 2009

Автор:

Себастьян Жан Роберт Дуранэ (Sbastien Jean Robert Doeraene) – sjrd.developpez.com

Оригинал учебника:

на французском – sjrd.developpez.com/delphi/tutoriel/generiques

на английском – sjrd.developpez.com/delphi/tutoriel/generics

Перевод:

Алексей Тимохин – TDelphi.blogspot.com www.tdelphiblog.com/2009/10/generics-delphi-2009-win32.html

Вёрстка, оформление, коррекция, перевод послесловия (X-XII):

Андрей Тишкин – InfoDelphi.ru

-1Огл вление Примеч ни переводчик I. Введение I-A. Предпосылки I-B. Что т кое дженерики?

II. Повседневное испол зов ние н примере TListT II-A. Простой код, дл н ч л II-B. Присвоени между о о щёнными кл сс ми II-C. Методы TListT II-D. TListT и комп р торы II-D-1. Пишем комп р тор с испол зов нием TComparerT II-D-2. Пишем комп р тор, испол зу функцию ср внени III. Созд ние о о щённого кл сс екл р ци кл сс III-A.

III-B. Ре лиз ци метод III-C. Псевдо-процедур Default III-D. Процедурные ссылки и о ход дерев III-D-1. А нонимные процедуры испол зуютс в других метод х?

III-E. И ост л ное III-F. К к испол зов т TTreeNodeT?

IV. Созд ние о о щённой з писи V. Огр ничени о о щенных типов V-A. К кие огр ничени можно испол зов т ?

V-B. З чем нужны огр ничени ?

V-C. В ри нт с конструктором VI. Испол зов ние в к честве п р метр олее чем одного тип VII. ругие типы дженериков

-2VIII. О о щённые методы VIII-A. О о щённ функци Min VIII-B. Перез грузк и огр ничени VIII-C. Некоторые дополнени дл TListT IX. RTTI и дженерики IX-A. Изменени в псевдо-процедуре TypeInfo IX-A-1. Более о щ функци TypeInfo IX-B. Ест ли RTTI у о о щённых типов?

X. Послесловие XI. З грузите исходный код XII. Бл год рности

-3Примеч ни переводчик Переводить это пособие оказалось не самым приятным занятием. Причина в том, что оригинальный текст был написан на французском, потом кое-как переведн на английский самим автором.

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

Другая часть работы вызвавшая у меня трудности – это адекватный перевод терминов. Буду признателен за замечания и уточнения.

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

Терминология:

actual types – фактические типы;

anonymous routines - анонимные методы;

callback – обратный вызов, ноя оставил его непереведнным;

сast – приведение типа;

class type – классовый тип, иногда просто класс (так как класс сам по себе является типом);

comparer – компаратор;

constraint – ограничение;

generic class – обобщнный класс;

generic parameter – обобщнный параметр;

generics – дженерики, генерики, параметризованные классы, шаблоны. Мне кажется, что наиболее подходящим для перевода словом в русском языке будет обобщения, но в переводе в основном использовал термин дженерики;

implicit conversion - неявное приведение (типа);

instanciate – создание экземпляра объекта;





interface type – интерфейсный тип (данных);

ordinal type – порядковый тип (данных);

pre-order walk – префиксный обход (дерева);

prerequisites – предварительные условия;

routine references - процедурные ссылки;

–  –  –

Это учебное пособие предназначено для программистов с хорошим знанием языка Delphi и требует углублнного понимания принципов объектно-ориентированного программирования.

Для понимания этого пособия предварительные знания о дженериках необязательны. На эту тему уже написана масса статей по другим языкам и я не буду рассматривать в деталях вопросы типа: «Как правильно применять дженерики?». В следующей части читателю будет представлено краткое введение в дженерики, но наиболее полное понимание скорее всего придт по мере чтения остальных частей пособия и изучения примеров.

Вот некоторые русскоязычные статьи раскрывающие тему дженериков в других языках/платформах, суть везде одна и та же:

Обобщнное программирование

–  –  –

Обобщенное программирование под.NET Уроки по С++. Урок 29. Использование шаблонов функций I-B. Что т кое дженерики?

Дженерики ещ иногда называют обобщнными параметрами. Это название чуть лучше раскрывает суть дженериков. В отличие от параметров функций (аргументы), которые имеют значения (values), обобщнные параметры являются типами. И они используются как параметры для классов, интерфейсов, записей или (реже) методов.

Чтобы стало понятнее, вот пример части класса TListT, который можно найти в модуле Generics.Collections.pas.

Да, в имени модулей разрешается использовать точки (кроме.pas, естественно). Это не имеет ничего общего с концепцией имен в. NET, как и с пакетами в Java. Точки - это то же самое, что и _ (символ подчркивания): просто часть названия.

type TListT = class(TEnumerableT) //...

public //...

function Add(const Value: T): Integer;

procedure Insert(Index: Integer; const Value: T);

function Remove(const Value: T): Integer;

procedure Delete(Index: Integer);

procedure DeleteRange(AIndex, ACount: Integer);

function Extract(const Value: T): T;

procedure Clear;

property Count: Integer read FCount write SetCount;

property Items[Index: Integer]: T read GetItem write SetItem; default;

end;

Может Вы уже обратили внимание на загадочность типа Т. Но действительно ли это тип? Нет, это обобщнный параметр. Если мне нужен список целых чисел (Integer), то имея такой класс в своем распоряжении, я буду использовать TListInteger, а не "обычный" TList, наряду с кучей приведений типов (casts) в коде!

-5Дженерики (generics) таким образом позволяют объявить набор (потенциальных) классов используя единственный исходный код, или, более официально, шаблон класса, который можно использовать с одним или несколькими типами-параметрами, по желанию. Каждый раз, когда инстанциируется новый фактический тип, вы как-будто создате отдельный класс, по его шаблону.

Как я сказал ранее, я не буду распространяться на темы того, почему и как работают генерики. Я сразу же перейду к повседневной работе с ними. Если Вам кажется, что у Вас вс ещ нет полного понимания того о чм я говорю, не беспокойтесь: все станет яснее в ближайшее время. Вы также можете прочитать (на французском языке) туториал Les gnriques SOUS Delphi. NET, который написал Лоран Дарденн (Laurent Dardenne).

-6II. Повседневное испол зов ние н примере TListT Парадоксально, но мы начнем с примера повседневного использования обобщнных классов (более корректно говорить: обобщнный шаблон класса), вместо дизайна такого типа. Есть несколько веских причин для этого.

Во-первых, значительно легче написать класс, когда у вас есть достаточно четкое представление о том, каким образом вы собираетесь его использовать. Это еще более очевидно в случаях, когда вы открываете для себя новые парадигмы программирования.

Во-вторых, большинство презентаций объектно-ориентированного программирования, как правило, сначала объясняют, как использовать имеющиеся классы.

II-A. Простой код, дл н ч л

Давайте постепенно начнем. Давайте напишем небольшую программу вычисляющую квадраты целых чисел от 0 до X. X будет задаваться пользователем.

Без дженериков, мы бы могли использовать динамический массив (и это было бы гораздо лучше, просто помните о том, что это учебный пример), но мы будем использовать список целочисленных чисел (integer).

Вот код:

program TutoGeneriques;

{$APPTYPE CONSOLE} uses SysUtils, Classes, Generics.Collections;

procedure WriteSquares(Max: Integer);

var List: TListInteger;

I: Integer;

begin List := TListInteger.Create;

try for I := 0 to Max do List.Add(I*I);

–  –  –

begin try WriteLn('Please enter a natural number:');

ReadLn(Max);

WriteSquares(Max);

except on E:Exception do Writeln(E.Classname, ': ', E.Message);

end;

ReadLn;

end.

-7На что здесь стоит обратить внимание?

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

var List: TListInteger;

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

Некоторые языки позволяют использовать выведение типа (примечание переводчика:type inference;

также смотрите комментарий от Alex Neznanov), позволяя компилятору угадывать тип параметра.

Но в Delphi Win32 такой фокус не пройдт. Поэтому нельзя писать так:

var List: TListInteger;

begin List := TList.Create; // здесь не хватает Integer...

end;

Во-вторых, что более важно, но менее заметно, поскольку речь идт о том, что фактически отсутствует в коде. А именно cast. Дело в том, что нет необходимости приводить тип Integer к типу Pointer! Удобно, не правда ли? Кроме того, такой код легче читать.

Ещ одно преимущество – это более высокая безопасность при приведении типов при использовании дженериков. Используя приведения типов, всегда остатся риск совершить ошибку, например, добавив Pointer в список, предназначенный для хранения integer, или наоборот. Если правильно использовать дженерики, то Вам, скорее всего не придтся прибегать к приведению типов вообще, или всего пару раз. Таким образом, шанс сделать ошибку уменьшается. Более того, даже если попытаться добавить Pointer к объекту класса TListInteger, компилятор откажется компилировать такой код. Без дженериков, такую ошибку можно было бы выявить только по абсурдному значению, может быть даже спустя месяцы после релиза программы.

Я использовал в примере тип Integer, чтобы ограничить код, не связанный непосредственно с представлением дженериков, но на деле, вместо Integer можно использовать любой тип данных.

II-B. Присвоени между о о щёнными кл сс ми

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

Начнм с понимания того принципа, что с дженериками мы не можем сделать ничего подобного!

Код в следующем примере являются недействительным и не скомпилируется:

var NormalList: TList;

IntList: TListInteger;

ObjectList: TListTObject;

ComponentList: TListTComponent;

begin ObjectList := IntList;

ObjectList := NormalList;

NormalList := ObjectList;

ComponentList := ObjectList;

ObjectList := ComponentList; // да, даже это не будет работать end;

-8Как указано в комментарии, несмотря на тот факт, что TComponent являются наследниками от TObject, переменная типа TListTComponent не может быть присвоена переменной типа TListTObject. Для того чтобы понять почему так происходит, представьте, что если бы присвоение работало, то TListTObject.Add(Value: TObject) позволило бы добавлять значениеTObject в список, состоящий из типовTComponent!

Другая важная вещь, на которую стоит обратить внимание, что TListT не является ни особым случаем, ни обобщением от TList. На самом деле, это совершенно разные типы — первый объявлен в модуле Generics.Collections, а последний в модуле Classes!

II-C. Методы TListT Примечательно, что TListT не предоставляет тот же набор методов, как TList. В нашем распоряжении есть больше методов "высокого уровня", но менее "низкоуровневых " методов (например, таких, как Last, который отсутствует).

Новые методы:

3 перегруженные версии методов AddRange, InsertRange и DeleteRange: они эквивалентны методам

Add/Insert/Delete соответственно, но работают со списками элементов:

o (const Values: array of T) o (const Collection: IEnumerableT) · (Collection: TEnumerableT) Метод Contains;

Метод LastIndexOf;

–  –  –

2 перегруженные версии Sort (это название не является новым, но способ использования отличается);

2 перегруженные версии метода BinarySearch.

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

В самом деле, в чм сегодня смысл добавления метода AddRange к устаревшему классу TList Никакого. Потому что каждый элемент должен подвергаться преобразованию типа. Поэтому, вам вс равно нужно было бы писать цикл, формирующий массив (с преобразованием типов на каждой итерации), для передачи его (массива) в AddRange. Вместо этого гораздо проще просто вызывать Add в каждой итерации.

Между тем, однажды написанный код с использованием дженериков, остатся действительно полезным и для других типов данных.

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

II-D. TListT и комп р торы

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

-9Ответ – компараторы. Компаратор является интерфейсом типа IComparerT. Этот тип объявлен в Generics.Defaults.pas.

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

Используемый по умолчанию компаратор будет зависеть от типа элемента. Чтобы получить его, TListT вызывает метод класса TComparerT.Default. Этот метод с помощью грязных манипуляций с RTTI, пытается получить наиболее подходящее решение, которое, однако, не всегда является подходящим.

Стандартный компаратор можно использовать для следующих типов данных:

Порядковые (ordinal) типы (целые числа, символы, Булевый тип, перечисления);

Типы с плавающей запятой;

Множества (для проверки на равенство);

Длинные юникодные строки (string, UnicodeString и WideString);

Длинные ANSI строки (AnsiString), но без учта кодовой страницы;

Типы variant (только для проверки на равенство);

Классы (только для проверки на равенство: используется TObject.Equals метод);

Указатели, мета-классы и интерфейсные типы (только для проверки на равенство);

Статические и динамические массивы, состоящие из ordinal типов, чисел с плавающей запятой или множеств (только проверка на равенство).

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

Для этого существуют два простых способа. Первый основан на написание функции, другой на наследовании класса TComparerT. Мы проиллюстрируем оба, на примере реализации сравнения для точки (TPoint). Предположим, что точки сравниваются согласно их удалнности точки-источника (0, 0), для того иметь возможность провести линейное упорядочивание (в математическом смысле).

II-D-1. Пишем комп р тор с испол зов нием TComparerT

Это очень просто! Вс что нужно сделать – это переопределить метод: Compare. Он должен вернуть:

1. 0 в случае равенства,

2. положительное целое число, если первый параметр больше, чем второй,

3. отрицательное целое если первый параметр меньше второго.

Вот результат:

function DistanceToCenterSquare(const Point: TPoint): Integer; inline;

begin Result := Point.X*Point.X + Point.Y*Point.Y;

end;

type TPointComparer = class(TComparerTPoint) function Compare(const Left, Right: TPoint): Integer; override;

end;

- 10 function TPointComparer.Compare(const Left, Right: TPoint): Integer;

begin Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);

end;

Обратите внимание на родителя класса TPointComparer: он наследуется от TComparerTPoint. Как вы видите, вполне возможно наследовать обычный класс от обобщнного класса.

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

Вот небольшая программа, которая создает 10 случайных точек, упорядочивает и выводит упорядоченный список:

function DistanceToCenter(const Point: TPoint): Extended; inline;

begin Result := Sqrt(DistanceToCenterSquare(Point));

end;

procedure SortPointsWithTPointComparer;

const MaxX = 100;

MaxY = 100;

PointCount = 10;

var List: TListTPoint;

I: Integer;

Item: TPoint;

begin List := TListTPoint.Create(TPointComparer.Create);

try for I := 0 to PointCount-1 do List.Add(Point(Random(2*MaxX+1) - MaxX, Random(2*MaxY+1) - MaxY));

List.Sort; // там используется компаратор, который был передан в конструктор for Item in List do WriteLn(Format('%d'#9'%d'#9'(distance to origin = %.2f)', [Item.X, Item.Y, DistanceToCenter(Item)]));

finally List.Free;

end;

end;

begin try Randomize;

SortPointsWithTPointComparer;

except on E:Exception do Writeln(E.Classname, ': ', E.Message);

end;

ReadLn;

end.

Подождите минуту! А где же освобождается наш компаратор? Напомним, что TListT принимает интерфейс от типа IComparerT, а не класс. Благодаря механизму подсчта ссылок для интерфейсов, который корректно реализован в TComparerT, компаратор будет автоматически освобождн, когда он перестанет быть нужным. Если вы не знаете ничего об интерфейсах в Delphi, я советую прочитать статью Программирование на языке Delphi: Глава 6. Интерфейсы (на русском языке).

- 11 II-D-2. Пишем комп р тор, испол зу функцию ср внени Этот вариант кажется более простым, поскольку как понятно из названия, нет необходимости возиться с дополнительными классами. Тем не менее, я решил представить его вторым, потому он знакомит с новым типом данных, появившимся в Delphi 2009 – процедурными ссылками (routine references).

Пытливый читатель сейчас, наверное, захочет возразить, что он уже давно знаком с процедурными ссылками. Хммм… навряд ли. ;-) Скорее всего, читатель подразумевает сейчас процедурные типы (procedural types), например, TNotifyEvent.

type TNotifyEvent = procedure(Sender: TObject) of object;

Процедурные ссылки же объявляются по-другому. Вот пример для TComparisonT:

type TComparisonT = reference to function(const Left, Right: T): Integer;

Между процедурными ссылками и процедурными типами есть как минимум три отличия:

Во-первых, процедурные ссылки не могут быть методами объекта (помечены as object). Иными словами, вы никогда не сможете присвоить процедурной ссылке метод объекта, а только процедуры или функции. (По крайней мере, мне так и не удалось добиться успеха, пытаясь это сделать ^ ^.) Второе отличие более фундаментально: в то время как процедурные типы (не методы объекта) является указателем на базовый адрес процедуры (точку входа процедуры), процедурные ссылки, по сути, являются интерфейсом! Со всеми делами, вроде подсчета ссылок и всех остальных штук.

Однако, Вас это, вероятно, не будет заботить, поскольку повседневное использование не отличается от использования процедурных типов.

И наконец (и это объяснит появление процедурных ссылок), существует возможность назначить анонимную процедуру (мы сейчас посмотрим, на что это похоже) для процедурной ссылки, но не для процедурного типа. Если Вы попробуете так сделать, то получите сообщение об ошибке от компилятора. Кстати, это также объясняет, почему процедурные ссылки реализованы как интерфейсы, но упоминание об этом не входит в рамки этого руководства. (Примечание переводчика: в оригинале было «Incidentally, that explains also why routine references are implemented as interfaces, but thoughts on this subject are not within the framework of this tutorial.») Давайте же вернемся к нашей сортировке точек. Чтобы создать компаратор на основе функции, мы используем другой метод класса TComparerT, - классовую функцию Construct. Этот классовый метод принимает в качестве параметра ссылку на функцию типа TComparisonT. Как было уже сказано, использование процедурных ссылок очень похоже на использование процедурных типов: в качестве параметра можно передавать название процедуры.

Вот код:

function ComparePoints(const Left, Right: TPoint): Integer;

begin Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);

end;

procedure SortPointsWithComparePoints;

const MaxX = 100;

MaxY = 100;

PointCount = 10;

var List: TListTPoint;

I: Integer;

Item: TPoint;

–  –  –

List.Sort; // используется компаратор переданный в конструктор for Item in List do WriteLn(Format('%d'#9'%d'#9'(distance to origin = %.2f)', [Item.X, Item.Y, DistanceToCenter(Item)]));

finally List.Free;

end;

end;

begin try Randomize;

SortPointsWithComparePoints;

except on E:Exception do Writeln(E.Classname, ': ', E.Message);

end;

ReadLn;

end.

Единственное место, которое изменилось – это создание компаратора. Остальной код использования списка, в целом, остался таким, как и раньше.

Внутри метода Construct создается экземпляр TDelegatedComparerT, который принимает в качестве параметра своего конструктора ссылку на процедуру (процедурную ссылку), которая будет отвечать за сравнение. Таким образом, вызов Construct вернт объект того же типа, который инкапсулирован в интерфейсе IComparerT. (примечание переводчика: в оригинале было «Calling Construct thus returns an object of this type, encapsulated in an interface IComparerT.») Ну, это тоже очень просто. По сути, мы должны помнить о том, что обобщения (дженерики) предназначены для того, чтобы сделать жизнь легче!

Есть ещ кое-что: существует возможность присвоить анонимную процедуру анонимной ссылке.

Давайте посмотрим, как это будет выглядеть:

procedure SortPointsWithAnonymous;

var List: TListTPoint;

//...

begin List := TListTPoint.Create(TComparerTPoint.Construct( function(const Left, Right: TPoint): Integer begin Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);

end));

// Always the same...

end;

Такой способ создания интересен в основном в тех случаях, когда процедура сравнения будет использоваться только в одном месте.

- 13 К слову об анонимных процедурах: да, они имеют доступ к локальным переменным той процедуры/метода, в котором они объявлены (примечание переводчика: в оригинале использовалось слово enclosing - заключены; см. также комментарий). И да, они могут делать это даже после того, как процедура или метод, в которой они были объявлены (примечание переводчика: здесь тоже было enclosing), закончил выполнение.

Следующий пример проиллюстрирует это:

function MakeComparer(Reverse: Boolean = False): TComparisonTPoint;

begin

Result :=

function(const Left, Right: TPoint): Integer begin Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);

if Reverse then Result := -Result;

end;

end;

procedure SortPointsWithAnonymous;

var List: TListTPoint;

//...

begin List := TListTPoint.Create(TComparerTPoint.Construct( MakeComparer(True)));

// Always the same...

end;

Интересно, не так ли?

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

- 14 III. Созд ние о о щённого кл сс Теперь, когда мы знаем, как использовать обобщнные классы, пришло время рассмотреть их внутреннее устройство.

Мы разработаем класс TTreeNodeT, который станет обобщнной реализацией дерева. На этот раз, это будет не просто обучение. Мы создадим реальный класс, который Вы сможете использовать в своих реальных проектах.

III-A. екл р ци кл сс

Давайте же начнем с самого начала: с объявления класса. Как Вы, наверное, помните из объявления TListT, обобщнный параметр (традиционно называемый Т, когда нет более точного названия) добавляется между угловыми скобками.

Вот так:

unit Generics.Trees;

type TTreeNodeT = class(TObject) end;

Узел дерева будет помечен значением типа T, и сможет иметь 0 и более детей. При уничтожении узла, он будет освобождать всех своих детей.

Что за значение типа T? Это также просто, как и «FValue: T;», также как и для любого другого нормального типа. Для хранения списка детей, мы будем использовать TObjectListU, объявленный в Generics.Collections. Я использовал здесь U, чтобы не запутаться. На деле, U будет заменено на TTreeNodeT! Кстати да, разрешается использовать обобщнный класс, как фактический обобщнный параметр.

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

Наш класс будет иметь следующие поля:

type TTreeNodeT = class(TObject) private FParent: TTreeNodeT;

FChildren: TObjectListTTreeNodeT;

FValue: T;

end;

Странно? Если вдуматься, то не очень. Мы уже говорили ранее, что обобщнный тип, может быть заменн любым другим типом. Так почему бы не использовать класс обобщнного типа?

Хочу обратить Ваше внимание на тот факт, что в имени TTreeNodeT,T не является фактическим обобщнным параметром (если хотите, формальным параметр). Но внутри класса, он становится фактическим типом! Оно ведт себя точно также как и параметры процедуры. При объявлении (примечание переводчика: в оригинале было «In the signature»), у Вас есть формальные параметры, но в теле процедуры, с ними работают, как и с любыми другими локальными переменными. Вот почему, Вы можете использовать T как тип для FValue, или даже как актуальный параметр для параметризации TTreeNodeT, при объявлении FChildren.

Когда я сказал, чтобы мы можем использовать в качестве фактического параметра любой тип, я был не до конца честен. Это не всегда так. Например, в TObjectListT, T всегда должно быть классовым типом. Это объясняется тем, что TObjectListT имеет

- 15 ограничение на свой параметризуемый тип. Позже мы рассматрим его более подробно, но я хочу указать на него уже сейчас, поскольку именно сейчас мы используем TObjectListT. А используем мы именно его для того, чтобы извлечь пользу от автоматического освобождения объектов хранящихся в TObjectListT, чего не умеет делать TListT.

Помимо конструктора и деструктора, мы предоставим методы для обхода в глубину (префискного и постфиксного (примечание переводчика: в оригинале было pre-order и post-order)), а также методы для добавления/перемещения и удаления дочерних узлов. Собственно, добавление будет запрашивать ребнок, когда ему будет назначен его родитель.

Для обхода, нам понадобится call-back тип. Угадайте какой? Процедурные ссылки. Мы реализуем два способа (примечание переводчика: в оригинале, использовался термин overload - перегрузка. Но мне это режет слух, поэтому я использовал слово «способ», однако в скобках оставил упоминания об оригинальном термине) обхода: обход по узлам, и обход по значению узлов. Второй способ (overload), вероятно, будет использоваться чаще при использовании класса TTreeNodeT. Первый же предусмотрен для полноты и является более общим. Он будет использоваться всего в нескольких методах TTreeNodeT (например, во второй перегрузке (overload)).

Наконец, будет, конечно же, доступ к свойствам родителей, детей и значению. Вс это реализуется в коде, приведнном ниже. Как Вы можете заметить, там нет ничего нового, кроме упоминания T, где только можно.

Вот полное объявление класса (которое, можете быть уверены, я даю только после того как полностью реализовал класс^^).

type /// Процедурная ссылка для Call-back с единственным параметром TValueCallBackT = reference to procedure(const Value: T);

–  –  –

FDestroying: Boolean; /// Становится True во время уничтожения объекта procedure DoAncestorChanged;

function GetRootNode: TTreeNodeT;

function GetChildCount: Integer; inline;

function GetChildren(Index: Integer): TTreeNodeT; inline;

function GetIndexAsChild: Integer; inline;

function GetIsRoot: Boolean; inline;

function GetIsLeaf: Boolean; inline;

function GetDepth: Integer;

protected procedure AncestorChanged; virtual;

procedure Destroying; virtual;

procedure AddChild(Index: Integer; Child: TTreeNodeT); virtual;

procedure RemoveChild(Child: TTreeNodeT); virtual;

procedure SetValue(const AValue: T); virtual;

property IsDestroying: Boolean read FDestroying;

- 16 public constructor Create(AParent: TTreeNodeT; const AValue: T); overload;

constructor Create(AParent: TTreeNodeT); overload;

constructor Create(const AValue: T); overload;

constructor Create; overload;

destructor Destroy; override;

procedure AfterConstruction; override;

procedure BeforeDestruction; override;

procedure MoveTo(NewParent: TTreeNodeT; Index: Integer = -1); overload;

procedure MoveTo(Index: Integer); overload;

function IndexOf(Child: TTreeNodeT): Integer; inline;

procedure PreOrderWalk( const Action: TValueCallBackTTreeNodeT); overload;

procedure PreOrderWalk(const Action: TValueCallBackT); overload;

procedure PostOrderWalk( const Action: TValueCallBackTTreeNodeT); overload;

procedure PostOrderWalk(const Action: TValueCallBackT); overload;

property Parent: TTreeNodeT read FParent;

property RootNode: TTreeNodeT read GetRootNode;

property ChildCount: Integer read GetChildCount;

property Children[Index: Integer]: TTreeNodeT read GetChildren;

property IndexAsChild: Integer read GetIndexAsChild;

property IsRoot: Boolean read GetIsRoot;

property IsLeaf: Boolean read GetIsLeaf;

property Value: T read FValue write SetValue;

end;

Давайте обозначим, на что здесь стоит обратить внимание. Собственно, не на что, кроме того факта, что каждый параметр типа T объявляется как const. На момент написания, мы не знаем, чем станет T.

И соответственно, существует вероятность, что T будет одним из «тяжлым» типов (таким как строка или запись), для которых более эффективно использовать именно const. А так как const не имеет побочных эффектов (оно просто не дат эффекта при использовании «лгких» типов, таких как Integer), мы всегда будем использовать е при работе с обобщнными типами.

Концепцию тяжлых и лгких типов я придумал сам. Она неофициальная, поэтому никаких цитат и ссылок. Под тяжлым типом я подразумеваю, тип, чьи параметры (во время исполнения) лучше передавать как const всегда, когда это только возможно. По большому счту, это типы, которые занимают больше чем 4 байта (не считая float и Int64), а также типы, которые требуют инициализации. Такие как строки, записи, массивы, интерфейсы (не классы) и Variants.

Однако, если параметр имеет тип TTreeNodeT, мы не будем ставить Const. Действительно, независимо от того, чем является Т, TTreeNode T остатся классовым типом, который является «лгким».

III-B. Ре лиз ци метод Чтобы проиллюстрировать особенности реализации обобщнного метода, давайте рассмотрим простой метод GetRootNode.

Он написан следующим образом:

- 17 Корневой узел дерева @возвращает Корневой узел дерева *} function TTreeNodeT.GetRootNode: TTreeNodeT;

begin if IsRoot then Result := Self else Result := Parent.RootNode;

end;

Единственное на что здесь стоит обратить внимание, состоит в том, что каждый раз, когда Вы пишете реализацию обобщнного метода, необходимо добавлять T к имени класса. Это обязательное требование, потому что TTreeNode (без T), по сути является другим типом, который кстати может быть объявлен в том же модуле (юните).

III-C. Псевдо-процедур Default

Чтобы реализовать два конструктора, не имеющие параметра AValue, нам необходимо как-то инициализировать FValue. Естественно, но ведь нам неизвестен фактический тип значения, как мы можем указать значение по умолчанию, которое подошло бы для любого возможного типа?

Выход в добавлении новой псевдо-процедуры Default. Как и TypeInfo, эта псевдо-процедура будет принимать в качестве параметра идентификатор типа. А название обобщноого класса, по сути, и является таким идентификатором.

Это псевдо-процедура будет возвращать значение по умолчанию для указанного типа.

Поэтому можно будет писать так:

{* Создат узел с родителем и без значения @param AParent Parent *} constructor TTreeNodeT.Create(AParent: TTreeNodeT);

begin Create(AParent, Default(T));

end;

{* Создат новый узел без родителя и без значения *} constructor TTreeNodeT.Create;

begin Create(nil, Default(T));

end;

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

III-D. Процедурные ссылки и о ход дерев

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

Оба способа (overloads) обхода принимают в качестве параметра ссылку на процедуру. Обратите внимание, что и здесь мы использовали const параметр. Внимательный читатель, наверняка помнит о

- 18 том, что ссылки на процедуры, по сути, являются интерфейсами. А интерфейсы мы договорились считать «тяжлым» типом. Поэтому лучше использовать const, когда это возможно.

Кроме этого примера, о первом способе (там, где обход по узлам) сказать особо нечего:

{* Префиксный обход (preorder walk) узлов дерева @param Action Действие выполняемое для каждого узла *} procedure TTreeNodeT.PreOrderWalk(const Action: TValueCallBackTTreeNodeT);

var Index: Integer;

begin Action(Self);

for Index := 0 to ChildCount-1 do Children[Index].PreOrderWalk(Action);

end;

Для вызова call-back, мы написали точно такой же код, что и для процедурных типов, т.е. в той же форме, что и вызов процедуры, но используя ссылку на процедуру вместо е имени.

При реализации второго способа (overload), мы воспользуемся первым способом, а в качестве Action, будем передавать... анонимную процедуру!

{* Префиксный обход (preorder walk) по значениям узлов дерева @param Action Действие выполняемое для каждого значения узла *} procedure TTreeNodeT.PreOrderWalk(const Action: TValueCallBackT);

begin PreOrderWalk( procedure(const Node: TTreeNodeT) begin Action(Node.Value);

end);

end;

Разве не чудесный кусок кода у нас получился?

Вы можете сказать, что я непоследователен в том, что говорю о const параметрах, так как параметр Node в анонимной процедуре объявлен как const, хотя это явно классовый тип. Причина в том, что необходимо обеспечить совместимость с сигнатурой типа TValueCallBackTTreeNodeT, которая требует именно const параметр ;-).

III-D-1. А нонимные процедуры испол зуютс в других метод х?

Да, ещ в двух. DoAncestorChanged, который должен сделать префиксный обход для метода AncestorChanged. И BeforeDestruction, который должен обойти всех (do a preorder walk) детей вызвав метод Destroying.

{* Вызывает метод AncestorChanged для каждого потомка (префиксно) *} procedure TTreeNodeT.DoAncestorChanged;

begin PreOrderWalk( procedure(const Node: TTreeNodeT) begin Node.AncestorChanged;

end);

if not IsDestroying then PreOrderWalk(procedure(const Node: TTreeNodeT) begin Node.Destroying end);

if (Parent nil) and (not Parent.IsDestroying) then Parent.RemoveChild(Self);

end;

III-E. И ост л ное В остальном… Это классический пример ;-) Ничего нового. Я не буду распространяться на этот счет, но и полный источник доступен для скачивания, так как все другие, в конце учебника.

III-F. К к испол зов т TTreeNodeT?

Это, безусловно, приятно написать класс обобщнного дерева, но как его использовать?

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

procedure TestGenericTree;

const NodeCount = 20;

var Tree, Node: TTreeNodeInteger;

I, J, MaxDepth, Depth: Integer;

begin Tree := TTreeNodeInteger.Create(0);

try // Строим дерево MaxDepth := 0;

for I := 1 to NodeCount do begin Depth := Random(MaxDepth+1);

Node := Tree;

–  –  –

if TTreeNodeInteger.Create(Node, I).Depth MaxDepth then Inc(MaxDepth);

end;

// Показать дерево с помощью префиксного обхода Tree.PreOrderWalk( procedure(const Node: TTreeNodeInteger) begin Write(StringOfChar(' ', 2*Node.Depth));

Write('- ');

WriteLn(Node.Value);

end);

finally Tree.Free;

end;

end;

- 20 IV. Созд ние о о щённой з писи Давайте изучим кое-что ещ и попробуем разработать простенькую, но полезную запись TNullableT. Идея состоит в том, чтобы создать запись, которая сможет иметь значения типа T, либо nil. Вы наверное уже задумывались о необходимости такого типа, например для представления значений Null из баз данных.

Эта запись будет содержать два поля: FValue типа T, и булевое поле FIsNil. Для чтения полей, созданы два свойства. Мы будем использовать лишь операторы неявного приведения (типов), чтобы построить значения этого типа.

unit Generics.Nullable;

interface type TNullableT = record private FValue: T;

FIsNil: Boolean;

public class operator Implicit(const Value: T): TNullableT;

class operator Implicit(Value: Pointer): TNullableT;

class operator Implicit(const Value: TNullableT): T;

property IsNil: Boolean read FIsNil;

property Value: T read FValue;

end;

За дополнительной информацией о перегрузке операторов я советую обратиться к франкоязычной статье La surcharge d'oprateurs sous Delphi 2006 Win32, которую написал Laurent Dardenne (примечание переводчика: я не нашл аналогичного материала на русском языке).

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

Реализация трех операторов преобразования достаточно проста. Второй (с параметром Pointer) нужен для того, чтобы можно было делать присвоение : = nil.

uses SysUtils;

resourcestring sCantConvertNil = 'Cannot convert to nil';

sOnlyValidValueIsNil = 'The only valid value is nil';

class operator TNullableT.Implicit(const Value: T): TNullableT;

begin Result.FValue := Value;

Result.FIsNil := False;

end;

class operator TNullableT.Implicit(Value: Pointer): TNullableT;

begin Assert(Value = nil, sOnlyValidValueIsNil);

Result.FIsNil := True;

end;

class operator TNullableT.Implicit(const Value: TNullableT): T;

begin if Value.IsNil then raise EConvertError.Create(sCantConvertNil);

Result := Value.FValue;

end;

- 21 Использовать эту запись можно так:

var Value: Integer;

NullValue: TNullableInteger;

begin NullValue := 5;

WriteLn(Integer(NullValue));

NullValue := nil;

if NullValue.IsNil then WriteLn('nil') else WriteLn(NullValue.Value);

NullValue := 10;

Value := NullValue;

WriteLn(Value);

end;

Результат выполнения будет таким:

nil Вс понятно? Это просто чудесно, учитывая, что я не дал ни единого пояснения. Это лишний раз доказывает, что дженерики это также просто как пирог :-). (примечание переводочника: не знаю почему автор считает что пирог – это просто. Я как-то пробовал испечь один, и скажу честно программирование намного проще) Полный исходный код Generics.Nullable доступен для скачивания в конце учебника.

- 22 V. Огр ничени о о щенных типов Хорошо, теперь вы знаете основы. Настало время перейти к серьезным вещам, а именно ограничениям.

Как видно из названия, с их помощью можно ввести некоторые ограничения на фактические типы, которые разрешено использовать для параметризации обобщенного типа. Продолжая аналогию с параметрами функций: ограничение для типа — то же самое, что тип для переменной. Не понятно?

Хорошо, вот пример: если Вы укажете тип параметра функции, то Вы можете передать в качестве параметра только выражение, которое будет соответствовать этому типу. А когда Вы указываете ограничения на параметр обобщнного типа, Вы должны будете заменить его фактическим типом, удовлетворяющим этим ограничениям.

V-A. К кие огр ничени можно испол зов т ?

Существует не так много ограничений доступных для использования. Точнее, всего четыре вида.

Допустимые обобщнные типы можно ограничить следующими способами:

классовым типом, который является потомком от заданного класса;

тип интерфейса, являющимся потомком от заданного интерфейса или классом, реализующим этот интерфейс;

порядковым (ordinal), числом с плавающей запятой или записью;

классом, имеющим конструктор без аргументов.

Для того, чтобы наложить ограничение на обобщнный параметр, нужно написать:

type TStreamGenericTypeT: TStream = class end;

TIntfListGenericTypeT: IInterfaceList = class end;

TSimpleTypeGenericTypeT: record = class end;

TConstructorGenericTypeT: constructor = class end;

Эти синтаксис, соответственно, указывает, что T может быть заменен только на:

1. класс TStream или один из его потомков;

2. IInterfaceList или один из его потомков;

3. на порядковый (ordinal), число с плавающей точкой или запись (один ненулевой тип данных, следуя терминологии Delphi);

4. класс, обеспечивающий конструктор с нулевым числом аргументов.

Нужно использовать T: class вместо T: tobject... Понятно, что класс будет принят, но может возникнуть вопрос, почему отклоняется TObject.

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

- 23 ограничениями по классу и/или ограничениями по интерфейсу. Можно даже объединить ограничение по записи с одним или несколькими ограничениями по интерфейс, но я не мог себе представить реально существующий тип, который бы удовлетворял этим двум ограничениям сразу (в NET такое возможно, но не в текущей реализации Delphi для Win32)!

Может быть, (но это только мо предположение) это введено в преддверии будущих версий, где Integer, например, будет "реализовывать" интерфейс IComparableInteger.

В таком случае, он вполне сможет удовлетворить ограничению вроде T: record, IComparableT.

Кроме того, можно использовать в качестве ограничения обобщнный класс или интерфейс с заданным типом параметра. Этим параметром может быть само Т. Например, может потребоваться тип которого можно сравнить с собой (с заданным).

Тогда можно было бы написать:

type TSortedListT: IComparableT = class(TObject) end;

Дополнительно, если вы хотите, что бы T было классового типа, вы можете написать так:

type TSortedListT: class, IComparableT = class(TObject) end;

V-B. З чем нужны огр ничени ?

"Я думал, что генерики предназначены для того чтобы использовать один код сразу для всех типов.

Какой смысл в том, чтобы ограничивать используемые типы?" Ну, ограничения позволяют компилятору получить больше информации об используемых типах.

Например, ограничение T: class, позволяет компилятору узнать, что для переменной типа Т, разрешается вызывать метод Free. А из ограничения T: IComparableT сделать вывод, что разрешается использовать Left.CompareTo(Right).

Мы рассмотрим это на примере дочернего класса TTreeNodeT, вызываемого TObjectTreeNodeT: class. Следуя примеру TObjectListT: class, который обеспечивает автоматическое освобождение его элементов при разрушении, наш класс будет при уничтожении освобождать сво значение (labelled value).

На самом деле, там будет не так уж много кода, учитывая, что большую часть мы уже написали в нашем суперклассе.

type {* Generic tree structure whose values are objects When the node is destroyed, its labelled value is freed as well.

*} TObjectTreeNodeT: class = class(TTreeNodeT) public destructor Destroy; override;

end;

{--------------------} { TObjectTreeNodeT } {--------------------} {* [@inheritDoc] *}

- 24 destructor TObjectTreeNodeT.Destroy;

begin Value.Free;

inherited;

end;

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

Обратите внимание на две вещи. Во-первых, обобщнный класс может наследоваться от другого обобщенного класса, используя тот же обобщнный параметр (или без него).

Во-вторых, при реализации методов обобщнных классов с ограничениями, ограничения не должны повторяться (примечание переводчика: в оригинале было «must not be repeated», что правильнее перевести «запрещено повторять»).

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

V-C. В ри нт с конструктором

Возможно Вы захотите иметь конструктор без параметра AValue, чтобы иметь возможность самому создать объект для FValue вместо того, чтобы использовать Default(T) (последний вернт nil здесь, потому что T имеет ограничение только на классы). (примечание переводчика: я так и не понял, что хотел сказать автор этой фразой. Ссылка на оригинал. Однако, ограничения хорошо описаны в справке к Delphi).

Ограничение по Конструктору предназначена для этой цели, которая дает:

type {* Структура обобщнного дерева, чьими значениями являются объекты Когда узел создатся без значения, новое значение создатся с помощью Конструктора T по умолчанию (конструктора без аргументов).

Когда узел освобождается, значение также освобождается.

*} TCreateObjectTreeNodeT: class, constructor = class(TObjectTreeNodeT) public constructor Create(AParent: TTreeNodeT); overload;

constructor Create; overload;

end;

implementation {* [@inheritDoc] *} constructor TCreateObjectTreeNodeT.Create(AParent: TTreeNodeT);

begin Create(AParent, T.Create);

end;

{* [@inheritDoc] *} constructor TCreateObjectTreeNodeT.Create;

begin Create(T.Create);

end;

Опять же, если удалить ограничение по конструктору, то компилятор выдаст ошибку на вызове Т.Create.

- 25 VI. Испол зов ние в к честве п р метр олее чем одного тип Как вы уже, наверное, догадались, при параметризации обобщнного типа, можно использовать несколько типов. Каждый из них может иметь свой собственный набор ограничений.

Таким образом, класс TDictionaryTKey,TValue принимает два типа параметров. Первый тип для ключей, а второй – тип для элементов. Этот класс реализует хеш-таблицу.

Не стоит заблуждаться: TKey и TValue в действительности (формально) являются обобщнными параметрами, а не реальными типами. Не надо смешивать эти вещи из-за нотации.

В этом месте, синтаксис декларации довольно слаб. Здесь возможно отделить типы запятыми (,) или точкой с запятой (;). В случае, когда типов больше двух, даже смешанное использование разделителей будет принято. Как на уровне декларации так и на уровне реализации. Однако, при использовании обобщнных типов, вы должны использовать запятые!

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

Действительно, использование запятой будет означать дополнительное ограничение.

Итак, позвольте мне предложить Вам стилистическое правило, которое не является тем правилом, которому следуют в Embarcadero. Всегда используйте точку с запятой при объявлении обобщнного типа (в случаях использования ограничений), и используйте запятые в остальных местах (при реализации методов и использовании обобщнных типов).

Поскольку у меня нет лучшего примера обобщнного типа с двумя параметрами, чтобы предложить Вам, кроме TDictionaryTKey,TValue,я предлагаю Вам взглянуть на код этого класса (объявленного, как Вы, возможно, догадались, в Generics.Collections).

Вот выдержка из него:

type TPairTKey,TValue = record Key: TKey;

Value: TValue;

end;

// Hash table using linear probing TDictionaryTKey,TValue = class(TEnumerableTPairTKey,TValue) //...

public //...

procedure Add(const Key: TKey; const Value: TValue);

procedure Remove(const Key: TKey);

procedure Clear;

property Items[const Key: TKey]: TValue read GetItem write SetItem; default;

property Count: Integer read FCount;

//...

end;

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

Особенно когда есть несколько параметризуемых типов.

- 26 VII. ругие типы дженериков До сих пор мы рассмотрели лишь обобщнные классы и записи. Тем не менее, мы уже вплотную приблизились к некоторым обобщнным интерфейсам.

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

Таким образом, не представляется возможным объявить указатель обобщнного типа (generic pointer

type), ни множество обобщнного типа (generic set type):

type TGenericPointert = ^T; // ошибка компилятора TGenericSett = set of T; // ошибка компилятора

- 27 VIII. О о щённые методы Мы рассмотрели много возможностей, которые предоставляют дженерики, применительно к типам, определнным разработчиком. Настало время перейти к обобщнным методам.

Эту часть часто объясняют во многих презентациях и шаблонах по использованию дженериков в других языках. Но я предпочл начать с обобщнных типов, которые используются чаще всего.

VIII-A. О о щённ функци Min

Для того, чтобы лучше представить эту концепцию, мы напишем метод для класса TArrayUtils.MinT, который найдт и вернт самый маленький элемент массива. Для этого нам, конечно же, понадобится компаратор типа IComparerT.

Как и название обобщнного типа, название обобщнного метода должен сопровождаться обобщнным параметром, расположенным между угловыми скобками. Здесь, обобщнный тип является типом элемента массива.

type TArrayUtils = class public class function MinT(const Items: array of T;

const Comparer: IComparerT): T; static;

end;

К сожалению, не представляется возможным объявить глобальную процедуру с обобщнными параметрами. Вероятно, причина этого ограничения связана с параллелизмом в синтаксисе Delphi.NET, чтобы уменьшить усилия по разработке и поддержанию.

Чтобы компенсировать это ограничение, мы будем использовать классовые методы.

Лучше, чтобы они были объявлены с ключевым словом static. У него нет ничего общего с омонимичным ключевым словом в C++. Речь здесь идт о статическом связывании.

Это говорит о том, что в таком методе нет Self, и тогда вызываются виртуальные методы класса, или виртуальные конструкторы должны быть "виртуализированы".(примечание переводчика: я не понял, что здесь имелось в виду: «This is to say, in such a method, there is no Self, and then calls to virtual class methods, or to virtual constructors, are to "virtualised".»). Другими словами, это как если бы они больше не были виртуальными.

В результате, это приводит к подлинной глобальной процедуре, но в другом пространстве имн.

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

Таким образом, можно написать метод DummyT(Int: Integer):

Integer, который не имеет формального параметра, чей тип зависел бы от обобщнного параметра T. В C++, например, такое не получилось бы даже скомпилировать.

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

Наш метод Min будет выглядеть следующим образом:

class function TArrayUtils.MinT(const Items: array of T;

const Comparer: IComparerT): T;

var I: Integer;

–  –  –

Result := Items[Low(Items)];

for I := Low(Items)+1 to High(Items) do if Comparer.Compare(Items[I], Result) 0 then Result := Items[I];

end;

Ничего исключительного, как Вы можете видеть. ;-) VIII-B. Перез грузк и огр ничени Давайте воспользуемся преимуществом этого прекрасного примера, чтобы пересмотреть наши ограничения и сделать перегруженную версию для тех типов элементов, которые поддерживают интерфейс T (он объявлен в System.pas).

Перед этим, давайте добавим ещ одну перезагрузку, которая будет принимать процедурную ссылку типа TComparisonT. Помните, мы легко можем «преобразовать» функцию обратного вызова типа TComparisonT в интерфейс IComparerT, вызвав метод TComparerT.Construct.

Вы можете посмотреть на использование метода CompareTo на параметре Left. Такой вызов, допускается, только из-за наличия ограничения.

type TArrayUtils = class public class function MinT(const Items: array of T;

const Comparer: IComparerT): T; overload; static;

class function MinT(const Items: array of T;

const Comparison: TComparisonT): T; overload; static;

class function MinT: IComparableT( const Items: array of T): T; overload; static;

end;

class function TArrayUtils.MinT(const Items: array of T;

const Comparison: TComparisonT): T;

begin Result := MinT(Items, TComparerT.Construct(Comparison));

end;

class function TArrayUtils.MinT(const Items: array of T): T;

var Comparison: TComparisonT;

begin

Comparison :=

function(const Left, Right: T): Integer begin Result := Left.CompareTo(Right);

end;

Result := MinT(Items, Comparison);

end;

Обратите внимание на вызов MinT: указание актуального типа между угловыми скобками является обязательным даже там. Это не относится к другим языкам, таким как C++.

Теперь мы хотим предоставить четвртый перегруженный метод, снова только с одним параметром Items, но на этот раз с параметром T без ограничений. Эта версия должна будет использовать TComparerT.Default.

- 29 Но это невозможно! На самом деле, несмотря на то, что ограничения на типы будут измены, параметры (аргументы) остаются теми же самыми. Поэтому, такие перегруженные методы будут совершенно двусмысленны! Таким образом, следующее объявление приведт к ошибке компилятора:

type TArrayUtils = class public class function MinT(const Items: array of T;

const Comparer: IComparerT): T; overload; static;

class function MinT(const Items: array of T;

const Comparison: TComparisonT): T; overload; static;

class function MinT: IComparableT( const Items: array of T): T; overload; static;

class function MinT( const Items: array of T): T; overload; static; // ошибка компилятора end;

Следовательно, мы должны выбрать: отказаться от первого или второго, или же использовать другое имя. И до тех пор, пока базовые типы, такие как Integer не начнут поддерживать интерфейс IComparableT, Вам могут понадобиться оба, остатся выбрать использование другого имени;-) type TArrayUtils = class public class function MinT(const Items: array of T;

const Comparer: IComparerT): T; overload; static;

class function MinT(const Items: array of T;

const Comparison: TComparisonT): T; overload; static;

class function MinT: IComparableT( const Items: array of T): T; overload; static;

class function MinDefaultT( const Items: array of T): T; static; end;

class function TArrayUtils.MinDefaultT(const Items: array of T): T;

begin Result := MinT(Items, IComparerT(TComparerT.Default));

end;

Почему я явно привожу Default к типу IComparerT, несмотря на то, что он уже явно означает IComparerT? Потому что процедурные ссылки и перегруженные методы до сих пор не очень хорошо работают друг с другом, и компилятор, кажется, начинает путаться от такого смешения. Без приведения типов, компиляция не удатся...

VIII-C. Некоторые дополнени дл TListT

Несмотря на то, что TListT является очень хорошим нововведением, оно никогда не сможет предоставить более практичных методов.

Вот пример реализации.NET метода FindAll для TList T. Этот метод направлен на получение подсписка с помощью функции предиката. Под функцией предиката здесь подразумевается процедура обратного вызова, которая принимает элемент списка, и возвращает True, если он должен быть выбран, и False в обратном случае.

Мы определяем процедурную ссылку типа TPredicateT следующим образом:

unit Generics.CollectionsEx;

interface uses Generics.Collections;

- 30 type TPredicateT = reference to function(const Value: T): Boolean;

К сожалению, невозможно написать class helper для обобщнного классы, поэтому мы напишем метод класса FindAllT который реализует это. Поскольку мы лишены возможности использовать class helper, мы постараемся хотя бы использовать его преимущество в том, чтобы реализовать более общее решение, работающее с энумератором или перечисляемым типом. (примечание: в оригинале было «enumerator or enumerable») type TListEx = class public class procedure FindAllT(Source: TEnumeratorT; Dest: TListT;

const Predicate: TPredicateT); overload; static;

class procedure FindAllT(Source: TEnumerableT; Dest: TListT;

const Predicate: TPredicateT); overload; static;

end;

implementation class procedure TListEx.FindAllT(Source: TEnumeratorT; Dest: TListT;

const Predicate: TPredicateT);

begin while Source.MoveNext do begin if Predicate(Source.Current) then Dest.Add(Source.Current);

end;

end;

class procedure TListEx.FindAllT(Source: TEnumerableT; Dest: TListT;

const Predicate: TPredicateT);

begin FindAllT(Source.GetEnumerator, Dest, Predicate);

end;

end.

Можно использовать этот метод следующим образом:

Source := TListInteger.Create;

try Source.AddRange([2, -9, -5, 50, 4, -3, 7]);

Dest := TListInteger.Create;

try TListEx.FindAllInteger(Source, Dest, TPredicateInteger( function(const Value: Integer): Boolean begin Result := Value 0;

end));

for Value in Dest do WriteLn(Value);

finally Dest.Free;

end;

finally Source.Free;

end;

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

Задачу, дополнить этот класс другими методами наподобие FindAll я оставлю вам. :-)

–  –  –

IX-A. Изменени в псевдо-процедуре TypeInfo RTTI всегда начинается с псевдо процедуры TypeInfo. Вы наверное знаете, что определнные типы не могут передаваться в эту псевдо процедуру. Например, указатели. И поэтому, она никогда не возвращает nil.

Итак, можем ли мы вызвать TypeInfo для обобщнного типа T? Вопрос уместный: ведь T может быть указателем (не валидным для TypeInfo) или целым (integer) (валидный).

Ответом будет да, разрешается вызывать TypeInfo для обобщнного типа. Но что случится, если T окажется типом, который не имеет RTTI? Именно в этом единственном случае, TypeInfo вернт nil.

Чтобы проиллюстрировать этот феномен, вот метода небольшого класса, который печатает имя и тип типа, но делает это с помощью дженериков:

type TRTTI = class(TObject) public class procedure PrintTypeT; static;

end;

class procedure TRTTI.PrintTypeT;

var Info: PTypeInfo;

begin Info := TypeInfo(T); // warning ! Info may be nil here if Info = nil then WriteLn('This type has no RTTI') else WriteLn(Info.Name, #9, GetEnumName(TypeInfo(TTypeKind), Byte(Info.Kind)));

end;

Этот метод, довольно специфичен. В том смысле, что его реальный параметр в действительности передатся как параметризованный тип.

begin TRTTI.PrintTypeInteger;

TRTTI.PrintTypeTObject;

TRTTI.PrintTypePointer;

end;

Выполнение его дат результат:

Integer tkInteger TObject tkClass This type has no RTTI

–  –  –

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

–  –  –

class function TRTTI.TypeInfoT: PTypeInfo;

begin Result := System.TypeInfo(T);

end;

Вы можете использовать его так:

Info := TRTTI.TypeInfoPointer; // Info = nil // вместо:

Info := TypeInfo(Pointer); // ошибка компилятора, так как у указателя (Pointer) нет RTTI

IX-B. Ест ли RTTI у о о щённых типов?

На самом деле здесь два вопроса: есть ли RTTI у неопределнных обобщнных типов (тех, у которых не определн параметр T)? И есть ли RTTI у определнных обобщнных типов (тех, у которых T было заменено реальным типом, таким как Integer)?

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

Можно спросить себя, какое название будет у такого типа. Итак:

begin TRTTI.PrintTypeTComparisonInteger;

TRTTI.PrintTypeTTreeNodeInteger;

end;

И результат:

TComparisonSystem.Integer tkInterface TTreeNodeSystem.Integer tkClass Это показывает, что название в себя включает между угловыми скобками полное имя фактического типа, заменяя параметр типа.

Вы также можете заметить, что тип процедурной ссылки TComparisonT выдаст результат tkInterface, и это доказывает то, что это действительно интерфейс.

Вот и вс что я хотел сказать об RTTI и дженериках. Конечно же я не буду здесь рассказывать о других изменениях в RTTI появившихся в Delphi 2009 из-за юникодных строк.

- 33 X. Послесловие Это конец нашего путешествия среди дженериков с Delphi 2009. Если вы ещ не имели возможность поэкспериментировать с дженериками в других языках, оно могло оказать разрушительное воздействие. Но на самом деле они очень практичны и действительно изменяют жизнь разработчика, как только он немного поиграется с ними.

XI. З грузите исходный код

Исходный код всех примеров этого учебника (документирован на французском), прилагается:

Sources.zip XII. Бл год рности Я хотел бы поблагодарить Лорана Дарденн (Laurent Dardenne) и SpiceGuid за их многочисленные комментарии и исправления, которые позволили значительно улучшить данный учебник.

Выражаю благодарность также Тибо Кувелье (Thibaut Cuvelier) за его корректировки к моему переводу на английский.




Похожие работы:

«ВВЕДЕНИЕ Г ибкость представляет собой важный компонент общего физического состояния. К сожалению, в большинстве фитнес-программ развитие этого качества стоит отнюдь не на первом месте. Его незаслуженно обходят вниманием или вообще игнорируют. В то время как о пользе регулярной тренировки сердечно-сосудистой си...»

«ООО Активные Технологии, г. Санкт-Петербург, Россия 8-800-333-49-28 (звонок бесплатный по России) info@ultra-ever-dry.info u-e-d.ru Универсальные высокоэффективные эко-очистители ULTRA GUARD и ULTRA GU...»

«Роль и место энергоресурсосбережения в развитии децентрализованной энергетики в РС(Я) Докладчик: Генеральный директор С.И. Губский Якутск, 2013 Схема расположения локальных систем электроснабжения Общая площадь – 2,2 млн. кв.км. Численность населения – 97,8 тыс.чел. Кол-во ДЭС – 125 ед. (ДГ – 582 ед.) Установленная мощность – 187,2 МВт ТЭ...»

«1000000300_1783393 Арбитражный суд Московской области 107053, ГСП 6, г. Москва, проспект Академика Сахарова, д.18 http://asmo.arbitr.ru/ Именем Российской Федерации РЕШЕНИЕ г.Москва 01 апреля 2013 года Дело №А41...»

«TT Аксессуары Audi TT Coup Audi Оригинальные аксессуары Оригинальные аксессуары Audi 3 Оригинальные аксессуары Audi. Индивидуальны, как Ваша жизнь. Audi Audi TT Coup: это не только средство передвижения. Но и возможность продемонстрировать уникальность личности. Не...»

«Навесной вентилируемый фасад "Вентал" Содержание Введение Основные достоинства вентилируемых фасадов Ознакомление с проектом возведение НВФ "Вентал" Описание навесной фасадной системы Подготовительные работы Монтажные механизмы Разметка фасада Осмотр наружных стен здания и проведения испытания на "вырыв" Проверка геометричес...»

«Министерство образования и науки Российской Федерации федеральное государственное бюджетное образовательное учреждение высшего профессионального образования Московский государственный университет печати имени Ивана Федорова Н.Г. Каменская ТЕХНОЛОГИЯ КНИГОРАСПРОСТР...»

«Руководство по эксплуатации az Pantera 4502 с пакетом Comfort 1 Самоходный полевой опрыскиватель (Норма токсичности ОГ Euro 3A / Euro 3B) Перед первым вводом в эксплуатацию обязательно прочитайте настоящее руководство по эксплуатации MG5096 и в дальнейшем соблюдайте BAG0133.4 05.17 Printed in Germany его указан...»

«Краткое руководство по установке 00825-0107-4016, ред. CA Июль 2015 Уровнемеры по перепаду давления и системы разделительных мембран Rosemount 1199 Июль 2015 Краткое руководство по установке ПРИМЕЧАНИЕ В данном руководстве по установке представлены общие указания из Руководства по эксплуатации разделительных мембран Rosemount 1199 (документ н...»

«Постановление Совета министров Республики Крым от 21.07.2015 N 415 (ред. от 10.11.2015) Об утверждении Государственной программы развития здравоохранения в Республике Крым на 2015 2017 годы Документ предоставлен КонсультантПлюс www.consultant.ru Дата сохранения: 11.05.2016 Постановление Совета министров Республики Крым от 21.07.2015...»

«I 5 ИЮ Й14 Н № Председателю Законодательного собрания Ленинградской области ГУБЕРНАТОР ЛЕНИНГРАДСКОЙ ОБЛАСТИ С.М.Бебенину ] 91311, Санкт-Петербург, Суворовский пр., 67 Для телеграмм: Санкт-Петербург, 191...»

«Канд. фил. наук Вячеслав Демидов Пруссия. Откуда она взялась? Коронация Бранденбургский курфюрст Фридрих III был недоволен. Вокруг все становились королями: на польском троне воссел добрый знакомец, курфюрст Саксонский, на английском — прямой родственник, курфюрст Ганн...»








 
2017 www.book.lib-i.ru - «Бесплатная электронная библиотека - электронные ресурсы»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.