Как реализовать IEnumerable‹T›?

Как реализовать IEnumerable<T>?

Задний план

Допустим, у меня есть класс, который я хочу реализовать IEnumerable<T>:

TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>)
public
   { IEnumerable<T> }
   function GetEnumerator: IEnumerator<T>;
end;

var
    IEnumerable<TMouse> mices = TStackoverflow<TMouse>.Create;

у меня была бы реализация:

TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>)
public
   { IEnumerable<T> }
   function GetEnumerator: IEnumerator<T>;
end;

function TStackoverflow<T>.GetEnumerator: IEnumerator<T>;
begin
    Result := {snip, not important here};
end;

Теперь, чтобы быть хорошим программистом, я выберу, чтобы мой класс также поддерживал интерфейс IEnumerable:

TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>, IEnumerable)
public
   { IEnumerable<T> }
   function GetEnumerator: IEnumerator<T>;

   { IEnumerable }
   function GetEnumerator: IEnumerator;
end;

function TStackoverflow<T>.GetEnumerator: IEnumerator<T>;
begin
    Result := {snip, not important here};
end;

function TStackoverflow.GetEnumerator: IEnumerator;
begin
    Result := {snip, not important here};
end;

Те из вас, кто знает, к чему все идет, поймут, что внедрение IEnumerable — отвлекающий маневр; и нужно было сделать так или иначе.

Теперь возникает проблема

Этот код не компилируется, потому что:

function GetEnumerator: IEnumerator<T>;
function GetEnumerator: IEnumerator;

у меня есть два метода с одинаковой сигнатурой (на самом деле не с одной и той же сигнатурой, но достаточно одинаковой, чтобы Delphi не мог их различить):

E2254 Перегруженная процедура GetEnumerator должна быть помечена директивой overload.

Хорошо, хорошо, я отмечу их как overloads:

function GetEnumerator: IEnumerator<T>; overload;
function GetEnumerator: IEnumerator; overload;

Но это не работает:

E2252 Метод GetEnumerator с идентичными параметрами уже существует

Разрешение метода интерфейса

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

type
    TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>, IEnumerable)
    protected
        function GetEnumeratorTyped: IEnumerator<T>;
        function GetEnumeratorGeneric: IEnumerator;
    public
        function IEnumerable<T>.GetEnumerator = GetEnumeratorTyped;
        function IEnumerable.GetEnumerator = GetEnumeratorGeneric;
    end;

{ TStackoverflow }

function TStackoverflow<T>.GetEnumeratorGeneric: IEnumerator;
begin

end;

function TStackoverflow<T>.GetEnumeratorTyped: IEnumerator<T>;
begin

end;

За исключением того, что это тоже не компилируется по причинам, которые ускользают от меня:

E2291 Отсутствует реализация метода интерфейса IEnumerable.GetEnumerator.

Так что давай забудем IEnumerable

Притворитесь, что мне все равно, если мой класс не поддерживает IEnumerable, давайте удалим его как поддерживаемый интерфейс. На самом деле это ничего не меняет, так как IEnumerable<T> происходит от IEnumerble:

IEnumerable<T> = interface(IEnumerable)
  function GetEnumerator: IEnumerator<T>;
end;

поэтому метод должен существовать, а код, удаляющий IEnumerable:

type
    TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>)
    protected
        function GetEnumeratorTyped: IEnumerator<T>;
    public
        function IEnumerable<T>.GetEnumerator = GetEnumeratorTyped;
    end;

{ TStackoverflow }

function TStackoverflow<T>.GetEnumeratorTyped: IEnumerator<T>;
begin

end;

не компилируется по той же причине:

E2291 Отсутствует реализация метода интерфейса IEnumerable.GetEnumerator.

Хорошо, тогда забудьте об дженериках

Итак, давайте остановимся, посотрудничаем и послушаем. IEnumerable вернулся как старое новое изобретение:

type
    TStackoverflow = class(TInterfacedObject, IEnumerable)
    public
        function GetEnumerator: IEnumerator;
    end;

{ TStackoverflow }

function TStackoverflow.GetEnumerator: IEnumerator;
begin

end;

Отлично, это работает. Я могу получить поддержку своего класса IEnumerable. Теперь я хочу поддержать IEnumerable<T>.

Что приводит меня к моему вопросу:

Как реализовать IEnumerable‹T›?


Обновление: забыл прикрепить полный нефункциональный код:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
    TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>, IEnumerable)
    protected
        function GetEnumeratorTyped: IEnumerator<T>;
        function GetEnumeratorGeneric: IEnumerator;
    public
        function IEnumerable<T>.GetEnumerator = GetEnumeratorTyped;
        function IEnumerable.GetEnumerator = GetEnumeratorGeneric;
    end;

{ TStackoverflow<T> }

function TStackoverflow<T>.GetEnumeratorGeneric: IEnumerator;
begin

end;

function TStackoverflow<T>.GetEnumeratorTyped: IEnumerator<T>;
begin

end;

begin
  try
     { TODO -oUser -cConsole Main : Insert code here }
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

person Ian Boyd    schedule 24.10.2014    source источник
comment
Причина, по которой вы не можете перегрузить эти два метода, заключается в том, что они имеют идентичные списки параметров. Точнее вообще без параметров. Разрешение перегрузки не учитывает тип возвращаемого значения.   -  person David Heffernan    schedule 24.10.2014
comment
Вы можете заменить свой идентификатор TStackoverflow<T> на идентификатор TStackoverflowCollection<T>, поскольку IEnumerable обычно применяется для коллекций и объектов, состоящих из других объектов.   -  person umlcat    schedule 24.10.2014
comment
Один только этот вопрос заслуживает еще 5000 голосов. Меня до сих пор не удивляет, как Emba может превратить простую концепцию в такой ужасный кошмар для среднего программиста. Можно сравнить приведенный выше вопрос с простотой и элегантностью реализации IEnumerable<T> в C#. Тем более с yield return сейчас последнее делается буквально за минуты. Задать вопрос в одиночку — это в 10 раз больше времени, не говоря уже о времени, потраченном на то, чтобы задать этот вопрос на SO. (Да, я знаю, что yield return может подойти не во всех случаях, а yield return тогда не было).   -  person JensG    schedule 08.03.2017
comment
@JensG Я вообще перестал использовать дженерики с интерфейсами. Существуют иногда ошибки компилятора, когда вы делаете Build All, и компилятор не может найти класс, который вы создаете. Сделайте Build All еще раз, и это сработает. Компилятор Delphi попахивает малофункциональным, собранным воедино, запутанным беспорядком. Я бы хотел, чтобы в C# была такая же мощная библиотека виджетов, как в Delphi.   -  person Ian Boyd    schedule 09.03.2017


Ответы (3)


предложение разрешения метода может выполнять эту работу:

type
  TStackoverflow<T> = class(TInterfacedObject, IEnumerable<T>, IEnumerable)
  public
    { IEnumerable<T> }
    function GetEnumeratorGeneric: IEnumerator<T>;
    function IEnumerable<T>.GetEnumerator = GetEnumeratorGeneric;

    { IEnumerable }
    function GetEnumerator: IEnumerator;
    function IEnumerable.GetEnumerator = GetEnumerator;
  end;

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

Что странно. Я бы сказал, что это похоже на ошибку компилятора. Такое поведение сохраняется даже в XE7.

С другой стороны, по крайней мере, теперь у вас есть способ двигаться вперед.

Обновить

Хорошо, комментарий Стефана ниже приводит меня к тому, что я с опозданием понимаю, что происходит на самом деле. На самом деле вам нужно удовлетворить три метода интерфейса. Очевидно, что мы должны предоставить реализацию для IEnumerable.GetEnumerator, чтобы удовлетворить IEnumerable. Но так как IEnumerable<T> происходит от IEnumerable, то IEnumerable<T> содержит два метода, IEnumerable.GetEnumerator и IEnumerable<T>.GetEnumerator. Итак, что вы действительно хотите сделать, это что-то вроде этого:

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

type
  IBase = interface
    procedure Foo;
  end;

  IDerived = interface(IBase)
    procedure Bar;
  end;

  TImplementingClass = class(TInterfacedObject, IBase, IDerived)
    procedure Proc1;
    procedure IBase.Foo = Proc1;

    procedure Proc2;
    procedure IDerived.Bar = Proc2;
  end;

Теперь обратите внимание, что использование наследования интерфейса означает, что IDerived содержит два метода, Foo и Bar.

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

E2291 Отсутствует реализация метода интерфейса IBase.Foo

Что, конечно, кажется странным, потому что, конечно, пункт разрешения метода имел дело с этим. Ну нет, разрешение метода касалось Foo, объявленного в IBase, а не того, который был унаследован в IDerived.

В приведенном выше примере мы можем довольно легко решить проблему:

TImplementingClass = class(TInterfacedObject, IBase, IDerived)
  procedure Proc1;
  procedure IBase.Foo = Proc1;
  procedure IDerived.Foo = Proc1;

  procedure Proc2;
  procedure IDerived.Bar = Proc2;
end;

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

К сожалению, вы не можете сделать это с IEnumerable<T>, потому что унаследованный метод имеет то же имя, что и новый метод, представленный IEnumerable<T>.

Еще раз спасибо Стефану за то, что привел меня к просветлению.

person David Heffernan    schedule 24.10.2014
comment
Да, это действительно странно. Это также работает, если я оставлю function GetEnumerator: IEnumerator и использую разрешение метода только на function IEnumerable<T>.GetEnumerator = GetEnumeratorTyped; - person Ian Boyd; 24.10.2014
comment
Что ж, это разумно. Как написано в моем ответе, второй пункт разрешения метода не имеет реального влияния, и я оставил его там для симметрии. - person David Heffernan; 24.10.2014
comment
Причина в том, что на самом деле неуниверсальный GetEnumerator должен присутствовать для обоих интерфейсов, потому что универсальный IEnumerable<T> наследуется от неуниверсального. Если вы укажете предложение разрешения для IEnumerable.GetEnumerator, оно отсутствует для метода IEnumerable<T>.GetEnumerator: IEnumerator (тот, который он наследует от IEnumerable). Решение унаследовать универсальный интерфейс от неуниверсального интерфейса, работающего с TObject, было глупым еще тогда, потому что он был слепо скопирован из .NET, который имеет корневую систему типов. - person Stefan Glienke; 25.10.2014
comment
@Стефан, я не совсем понимаю твою точку зрения. Я думаю, мы понимаем наследование интерфейса. Но почему разрешение метода не справится с этим. Нужно ли нам реализовывать IEnumerable.GetEnumerator как для IEnumerable, так и для IEnumerable‹T›? - person David Heffernan; 25.10.2014
comment
Дело в том, что предложение разрешения метода для IEnumerable.GetEnumerator будет обрабатывать его только для самого IEnumerable, а не при доступе к нему из IEnumerable<T>. Но поскольку методы имеют одно и то же имя, вы не можете ссылаться на неуниверсальный унаследованный метод с помощью IEnumerable<T>.GetEnumerator. Предложение разрешения метода не применяет это разрешение для каких-либо других интерфейсов, наследуемых от типа интерфейса, для которого у меня есть некоторое предложение разрешения метода. Фактически это означает, что если вы реализуете IEnumerable и IEnumerable<T>, вам нужно реализовать 3 метода GetEnumerator. - person Stefan Glienke; 25.10.2014
comment
Если вы называете неуниверсальный GetEnumerator, а универсальный — другим и используете для него предложение разрешения, компилятор будет счастлив, потому что он может использовать его для обоих интерфейсов, но не наоборот. Вот почему ваше решение работает, а другое - нет. - person Stefan Glienke; 25.10.2014
comment
@StefanGlienke Наконец-то я понял. Я обновил ответ соответственно. Вы согласны с тем, что я правильно понял? - person David Heffernan; 25.10.2014
comment
@DavidHeffernan Точно! - person Stefan Glienke; 25.10.2014
comment
Это было похожее обсуждение в предыдущем вопросе, к которому я добавил ответ stackoverflow.com/questions/6389357/ Есть две проблемы, которые здесь не упоминались: 1: T не обязательно является TObject. Например, IEnumerable‹T› должен позволять вам перебирать целые числа. Но если T является целым числом, то IEnumerable не может работать. 2: Если вы попытаетесь выполнить итерацию, используя объект TStackoverflow‹T›, а не интерфейс, GetEnumerator будет неуниверсальным, и вы сможете выполнять итерацию только TObjects. - person urtlet; 23.01.2015

Быстрый короткий ответ

Существует ДВЕ группы интерфейсов с одинаковыми именами, например: неуниверсальные IEnumerable и: IEnumerable<T>.

Обзор

Есть такая же проблема.

Проблема не в реализации интерфейса или универсальной реализации интерфейса.

Существует особая ситуация с универсальными интерфейсами, такими как IEnumerable<T>, поскольку они также имеют неуниверсальный интерфейс IEnumerable. Эти интерфейсы предназначены для совместимости с ОС, а не просто еще один программный интерфейс.

Кроме того, есть несколько других универсальных и неуниверсальных родительских интерфейсов, таких как IEnumerator или IComparable, которые имеют свои собственные определения членов.

Решение

Чтобы решить ту же проблему, я смотрю, какие родительские интерфейсы есть у IEnumerable<T>, и обнаруживаю соответствующие неуниверсальные интерфейсы.

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

type
    // Important: This intermediate class declaration is NOT generic

    // Verify that non generic interface members implemented

    TStackoverflowBase = class(TInterfacedObject, IEnumerable)
    protected
        function GetEnumeratorGeneric: IEnumerator;
    public
        function IEnumerable.GetEnumerator = GetEnumeratorGeneric;
    end;

    // Important: This proposed class declaration IS generic,
    // yet, it descends from a non generic intermediate class.

    // After verify that non generic interface members implemented,
    // verify that generic interface members implemented,

    TStackoverflow<T> = class(TStackoverflowBase, IEnumerable<T>)
    protected
        function GetEnumeratorTyped: IEnumerator<T>;
    public
        function IEnumerable<T>.GetEnumerator = GetEnumeratorTyped;
    end;

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

Решение @David Heffernan, в котором есть только один класс, неплохое, я пробовал тот же подход, но он был более сложным.

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

Ваше здоровье.

person umlcat    schedule 24.10.2014
comment
Крутая вещь в такой реализации заключается в том, что вам вообще не нужен пункт разрешения. Таким образом, вы можете назвать оба метода GetEnumerator и все будет в порядке. - person Stefan Glienke; 25.10.2014
comment
FWIW, на самом деле это не имеет ничего общего с дженериками. Это видно из примера в моем ответе. - person David Heffernan; 27.10.2014

На самом деле, это очень просто, хотя и не совсем очевидно. Важно знать, что компилятор творит чудеса с перечислителями.

Во-первых, нам вообще не нужно реализовывать IEnumerator или IEnumerator<T>. Вместо этого мы объявляем или собственный интерфейс. Для этого примера мы перечисляем WideStrings, поэтому это может выглядеть так, как показано ниже. Обратите внимание, что здесь важны имена и подписи методов/свойств:

IFoobarEnumerator = interface
  function GetCurrent: WideString;
  function MoveNext: Boolean;
  procedure Reset;
  property Current: WideString read GetCurrent;
end;

Теперь окружающий класс получает метод с именем GetEnumerator, который возвращает что-то похожее на перечислитель.

Я намеренно написал «что-то, что похоже на перечислитель», а не «что-то, что реализует IEnumerator<T>», потому что это все, что нужно компилятору, чтобы волшебство произошло:

function  GetEnumerator : IFoobarEnumerator;  

Помимо того, что нам конечно еще нужно реализовать класс перечислителя для IFoobarEnumerator - вот и все. Были сделаны.

person JensG    schedule 08.03.2017
comment
Это не будет поддерживать этот синтаксис: для [элемент] в [перечисляемый класс] - person Magne Rekdal; 17.03.2017