Почему TEnumerable <T>использует сквозные методы? - PullRequest
12 голосов
/ 05 августа 2009

TEnumerable<T>, базовый класс для всех контейнерных классов Generics.Collections, имеет очень странное объявление. Это выглядит так:

type
  TEnumerable<T> = class abstract
  protected
    function DoGetEnumerator: TEnumerator<T>; virtual; abstract;
  public
    function GetEnumerator: TEnumerator<T>;
  end;

function TEnumerable<T>.GetEnumerator: TEnumerator<T>;
begin
  Result := DoGetEnumerator;
end;

TEnumerator<T> аналогичным образом объявляется открытый метод MoveNext и частная функция DoMoveNext, а MoveNext ничего не делает, кроме вызова DoMoveNext.

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

Ответы [ 3 ]

27 голосов
/ 05 августа 2009

Отказ от ответственности: Я написал TEnumerable<T>. Если бы я сделал это снова, я бы написал это с меньшей производительностью и большей простотой, поскольку я понял, что эта оптимизация сбивает с толку многих людей.

Он предназначен для предотвращения виртуального вызова в циклах for-in при сохранении совместимости с полиморфизмом. Это общая схема:

  • Базовый класс Base определяет защищенный виртуальный абстрактный метод V и открытый не виртуальный метод M. M отправляет V, поэтому полиморфные вызовы через переменную типа Base будут перенаправлять переопределенное поведение V.

  • Классы-потомки, такие как Desc, реализуют статическое переопределение M (скрытие Base.M), которое содержит реализацию, и реализуют переопределение V, которое вызывает Desc.M. Вызовы M через Desc -типовые переменные направляются непосредственно в реализацию без виртуальной отправки.

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

var
  someCollection: TSomeCollection<TFoo>;
  x: TFoo;
begin
  // ...
  for x in someCollection do
    // ...
end;

... компилятор ищет метод с именем GetEnumerator для статического типа someCollection и метод с именем MoveNext для возвращаемого типа (и аналогично для свойства Current). Если этот метод имеет статическую диспетчеризацию, виртуальный вызов может быть исключен.

Это наиболее важно для циклов и, таким образом, MoveNext / Current аксессора. Но для того, чтобы оптимизация работала, тип возврата метода GetEnumerator должен быть ковариантным, то есть он должен статически возвращать правильный производный тип перечислителя. Но в Delphi, в отличие от C ++ [1], невозможно переопределить метод предка с более производным типом возврата, поэтому тот же трюк необходимо применить по другой причине, чтобы изменить тип возврата у потомков.

Оптимизация также потенциально допускает встраивание вызовов методов MoveNext и GetCurrent, поскольку статическому компилятору очень трудно «видеть сквозные» виртуальные вызовы и при этом быть быстрыми.

[1] C ++ поддерживает ковариацию возвращаемых значений в переопределенных методах.

3 голосов
/ 05 августа 2009

Метод GetEnumerator () не является виртуальным; Вы не можете переопределить это. Это один из способов обеспечения того, чтобы GetEnumerator () всегда существовал, всегда принимал фиксированный набор параметров (в данном случае ни один) и чтобы какой-то программист не испортил его для классов-потомков. Любой, кто использует TEnumerable - или его потомок - может вызвать GetEnumerator ().

Но поскольку будут разные потомки TEnumerable, которые делают разные вещи, DoGetEnumerator () позволяет программисту вносить внутренние изменения в структуру. «Виртуальный» позволяет переопределить метод. «Абстрактный» заставляет классы-потомки реализовать метод - компилятор не позволит вам забыть. А поскольку DoGetEnumerator () объявлен как защищенный (по крайней мере, на этом уровне), программист, использующий потомок TEnumerable, не может обойти GetEnumerator () и напрямую вызвать DoGetEnumerator ().

3 голосов
/ 05 августа 2009

Вы не сможете выполнять такие трюки, как повторное введение GetEnumerator:

function TDictionary<TKey, TValue>.TKeyCollection.DoGetEnumerator: TEnumerator<TKey>;
begin
  Result := GetEnumerator;
end;
...
function TDictionary<TKey, TValue>.TKeyCollection.GetEnumerator: TKeyEnumerator;
begin
  Result := TKeyEnumerator.Create(FDictionary);
end;

для создания правильного локального / конкретного TEnumerator

...