Страницы

вторник, 4 августа 2009 г.

Генерики в Delphi 2009 для Win32. Часть 5. Ограничения обобщенных типов.

В пособии также рассматриваются анонимные методы и процедурные ссылки.



Словарь терминов
  • generics - дженерики, генерики, параметризованные классы, шаблоны, обобщения;
  • Типы данных: string – строка, record – запись;
  • implicit conversion - неявное приведение (типа);
  • constraint – ограничение;
  • actual types – фактические типы
  • generic class – обобщённый класс;
  • generic parameter – обобщённый параметр;
  • сast – приведение типа;
  • anonymous routines - анонимные методы;
  • routine references - процедурные ссылки;
  • ordinal type – порядковый тип (данных);
  • interface type – интерфейсный тип (данных);
  • class type – классовый тип, иногда просто класс (так как класс сам по себе является типом);
  • comparer – компаратор;
  • unit – модуль, юнит;
  • instanciate – создание экземпляра объекта;
  • ordinal type – порядковый тип (данных)

V. Ограничения обобщенных типов

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

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

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

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

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

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

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

type
  TStreamGenericType<T: TStream> = class
  end;

  TIntfListGenericType<T: IInterfaceList> = class
  end;

  TSimpleTypeGenericType<T: record> = class
  end;

  TConstructorGenericType<T: constructor> = class
  end;

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

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

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

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

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

warning

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

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

idea

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

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

type
  TSortedList<T: IComparable<T>> = class(TObject)
  end; 

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

type
  TSortedList<T: class, IComparable<T>> = class(TObject)
  end;
  

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

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

Ну, ограничения позволяют компилятору получить больше информации об используемых типах. Например, ограничение , позволяет компилятору узнать, что для переменной типа Т, разрешается вызывать метод Free. А из ограничения > сделать вывод, что разрешается использовать Left.CompareTo (Right).

Мы рассмотрим это на примере дочернего класса TTreeNode , названного TObjectTreeNode . Следуя примеру TObjectList , который обеспечивает автоматическое освобождение его элементов при разрушении, наш класс будет при уничтожении освобождать своё значение (labelled value).

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

type
  {*
    Generic tree structure whose values are objects
    When the node is destroyed, its labelled value is freed as well.
  *}
  TObjectTreeNode<T: class> = class(TTreeNode<T>)
  public
    destructor Destroy; override;
  end;

{--------------------}
{ TObjectTreeNode<T> }
{--------------------}

{*
  [@inheritDoc]
*}
destructor TObjectTreeNode<T>.Destroy;
begin
  Value.Free;
  inherited;
end;

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

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

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

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

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

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

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

type
    {*
      Структура обобщённого дерева, чьими значениями являются объекты
      Когда узел создаётся без значения, новое значение создаётся с помощью
      Конструктора T по умолчанию (конструктора без аргументов).
      Когда узел освобождается, значение также освобождается.
    *}
  TCreateObjectTreeNode<T: class, constructor> = class(TObjectTreeNode<T>)
  public
    constructor Create(AParent: TTreeNode<T>); overload;
    constructor Create; overload;
  end;

implementation

{*
  [@inheritDoc]
*}
constructor TCreateObjectTreeNode<T>.Create(AParent: TTreeNode<T>);
begin
  Create(AParent, T.Create);
end;

{*
  [@inheritDoc]
*}
constructor TCreateObjectTreeNode<T>.Create;
begin
  Create(T.Create);
end;
 

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

VI. Использование в качестве параметра более чем одного типа

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

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

warning

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

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

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

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

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

type
  TPair<TKey,TValue> = record
    Key: TKey;
    Value: TValue;
  end;

  // Hash table using linear probing
  TDictionary<TKey,TValue> = class(TEnumerable<TPair<TKey,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;
 
image

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

VII. Другие типы дженериков

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

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

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

type
  TGenericPointer<t> = ^T; // ошибка компилятора
  TGenericSet<t> = set of T; // ошибка компилятора
	



Публикации по теме

4 комментария:

  1. Имеются "косяки" в примечаниях, то есть вот в этих местах
    * Нужно использовать вместо
    * Может быть, (но это только моё предположение)
    * Как Вы уже заметили

    С угловыми скобками, которые превратились в теги и обрамлённый ими текст исчез из поля зрения :(

    ОтветитьУдалить
  2. TStringConvertor = class
    private
    public
    function Convert(Value:T):string;
    end;


    function TStringConvert.Convert(Value:T):string;
    begin

    result:=value; ? Подскажите как мне поступить тут??
    end

    ОтветитьУдалить
  3. Анонимный, с такими вопросами обращайтесь на форум.

    Здесь в комментах невозможно обсуждать код дженериков из-за того, что blogger обрезает текст в угловых скобках.

    ОтветитьУдалить
  4. отличная статя, спосибо!

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