Страницы

суббота, 26 сентября 2009 г.

Дженерики в Delphi 2009 для Win32. Часть 6. Обобщённые методы.

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



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

VIII. Обобщённые методы

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

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

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

Для того, чтобы лучше представить эту концепцию, мы напишем метод для класса TArrayUtils.Min<T>, который найдёт и вернёт самый маленький элемент массива. Для этого нам, конечно же, понадобится компаратор типа IComparer<T>.

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

type
  TArrayUtils = class
  public
    class function Min<T>(const Items: array of T;
      const Comparer: IComparer<T>): T; static;
  end;
image

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

image

Чтобы компенсировать это ограничение, мы будем использовать классовые методы. Лучше, чтобы они были объявлены с ключевым словом 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".»). Другими словами, это как если бы они больше не были виртуальными.
В результате, это приводит к подлинной глобальной процедуре, но в другом пространстве имён.

image

В отличие от других языков, параметр не обязательно должен использовать обобщённый тип. Таким образом, можно написать метод Dummy<T>(Int: Integer): Integer, который не имеет формального параметра, чей тип зависел бы от обобщённого параметра T. В C++, например, такое не получилось бы даже скомпилировать.

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

class function TArrayUtils.Min<T>(const Items: array of T;
  const Comparer: IComparer<T>): T;
var
  I: Integer;
begin
  if Length(Items) = 0 then
    raise Exception.Create('No items in the array');

  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).

Перед этим, давайте добавим ещё одно переопределение, которое будет принимать процедурную ссылку типа TComparison<T>. Помните, мы легко можем «преобразовать» функцию обратного вызова типа TComparison<T> в интерфейс IComparer<T>, вызвав метод TComparer<T>.Construct.

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

type
  TArrayUtils = class
  public
    class function Min<T>(const Items: array of T;
      const Comparer: IComparer<T>): T; overload; static;
    class function Min<T>(const Items: array of T;
      const Comparison: TComparison<T>): T; overload; static;
    class function Min<T: IComparable<T>>(
      const Items: array of T): T; overload; static;
  end;

class function TArrayUtils.Min<T>(const Items: array of T;
  const Comparison: TComparison<T>): T;
begin
  Result := Min<T>(Items, TComparer<T>.Construct(Comparison));
end;

class function TArrayUtils.Min<T>(const Items: array of T): T;
var
  Comparison: TComparison<T>;
begin
  Comparison :=
    function(const Left, Right: T): Integer
    begin
      Result := Left.CompareTo(Right);
    end;
  Result := Min<T>(Items, Comparison);
end;
image

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

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

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

type
  TArrayUtils = class
  public
    class function Min<T>(const Items: array of T;
      const Comparer: IComparer<T>): T; overload; static;
    class function Min<T>(const Items: array of T;
      const Comparison: TComparison<T>): T; overload; static;
    class function Min<T: IComparable<T>>(
      const Items: array of T): T; overload; static;
    class function Min<T>(
      const Items: array of T): T; overload; static; // ошибка компилятора
  end;

Следовательно, мы должны выбрать: отказаться от первого или второго, или же использовать другое имя. И до тех пор, пока базовые типы, такие как Integer не начнут поддерживать интерфейс IComparable<T>, Вам могут понадобиться оба, остаётся выбрать использование другого имени;-)

type
  TArrayUtils = class
  public
    class function Min<T>(const Items: array of T;
      const Comparer: IComparer<T>): T; overload; static;
    class function Min<T>(const Items: array of T;
      const Comparison: TComparison<T>): T; overload; static;
    class function Min<T: IComparable<T>>(
      const Items: array of T): T; overload; static;

    class function MinDefault<T>(
      const Items: array of T): T; static;
  end;

class function TArrayUtils.MinDefault<T>(const Items: array of T): T;
begin
  Result := Min<T>(Items, IComparer<T>(TComparer<T>.Default));
end;

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

VIII-C. Некоторые дополнения для TList<T>

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

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

unit Generics.CollectionsEx;

interface

uses
  Generics.Collections;

type
  TPredicate<T> = reference to function(const Value: T): Boolean;

К сожалению, невозможно написать class helper для обобщённого класса, поэтому мы напишем метод класса FindAll<T> который реализует это. Поскольку мы лишены возможности использовать class helper, мы постараемся хотя бы использовать его преимущество в том, чтобы реализовать более общее решение, работающее с энумератором или перечисляемым типом. (примечание: в оригинале было «enumerator or enumerable»)

type
  TListEx = class
  public
    class procedure FindAll<T>(Source: TEnumerator<T>; Dest: TList<T>;
      const Predicate: TPredicate<T>); overload; static;
    class procedure FindAll<T>(Source: TEnumerable<T>; Dest: TList<T>;
      const Predicate: TPredicate<T>); overload; static;
  end;

implementation

class procedure TListEx.FindAll<T>(Source: TEnumerator<T>; Dest: TList<T>;
  const Predicate: TPredicate<T>);
begin
  while Source.MoveNext do
  begin
    if Predicate(Source.Current) then
      Dest.Add(Source.Current);
  end;
end;

class procedure TListEx.FindAll<T>(Source: TEnumerable<T>; Dest: TList<T>;
  const Predicate: TPredicate<T>);
begin
  FindAll<T>(Source.GetEnumerator, Dest, Predicate);
end;

end.

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

Source := TList<Integer>.Create;
try
  Source.AddRange([2, -9, -5, 50, 4, -3, 7]);
  Dest := TList<Integer>.Create;
  try
    TListEx.FindAll<Integer>(Source, Dest, TPredicate<Integer>(
      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;
image

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

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



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

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

  1. Спасибо за работу Алексей.

    В "Публикации по теме" ссылка на 3-ю часть ведет на 2-ую.

    ОтветитьУдалить
  2. Добрый день

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

    У меня есть вопросы по этому поводу. Если не трудно, то свяжитесь со мной по ICQ: 334-403-320
    (связался бы сам с вами, да контактов ваших нигде не нашел)

    ОтветитьУдалить
  3. Добрый день.

    Мои контактные данные указаны в панели в правой части блога. =)

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