for-in loop/ru

From Free Pascal wiki
Jump to navigationJump to search

English (en) français (fr) 日本語 (ja) русский (ru)

"for-in" цикл появился в delphi, начиная с версии 2005. Данная конструкция доступна сейчас с версии fpc 2.4.2.

Delphi и FPC реализация

Она имеет следующий синтаксис:

Цикл применимый к строкам

procedure StringLoop(S: String);
var
  C: Char;
begin
  for C in S do
    DoSomething(C);
end;

Цикл применимый к массиву

procedure ArrayLoop(A: Array of Byte);
var
  B: Byte;
begin
  for B in A do
    DoSomething(B);
end;

Цикл применимый к множеству

type
  TColor = (cRed, cGren, cBlue);
  TColors = set of TColor;
procedure SetLoop(Colors: TColors);
var
  Color: TColor;
begin
  for Color in Colors do
    DoSomething(Color);
end;

Применение к классам контейнерам

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

TSomeEnumerator = class
public
  function MoveNext: Boolean;
  property Current: TSomeType;
end;

Для реализации перечислителя необходимы: метод MoveNext, который увеличивает позицию элемента перечисления и свойство Current, в котором возвращается выбранный вами тип данных.

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

Пример:

type
  TEnumerableTree = class;

  TTreeEnumerator = class
  private
    FTree: TEnumerableTree;
    FCurrent: TNode;
  public
    constructor Create(ATree: TEnumerableTree); 
    function MoveNext: Boolean;
    property Current: TNode read FCurrent;
  end;

  TEnumerableTree = class
  public
    function GetEnumerator: TTreeEnumerator;
  end;

constructor TTreeEnumerator.Create(ATree: TEnumerableTree);
begin
  inherited Create;
  FTree := ATree;
  FCurrent := nil;
end;

function TTreeEnumerator.MoveNext: Boolean;
begin
  // получение следующего узла дерева
  if FCurrent = nil then
    FCurrent := FTree.GetFirstNode
  else
    FCurrent := FTree.GetNextNode(FCurrent);
  Result := FCurrent <> FTree.GetLastNode;
end;

function TEnumerableTree.GetEnumerator: TTreeEnumerator;
begin
  Result := TTreeEnumerator.Create(Self);
end;

После этого вы можете использовать следующий код:

procedure TreeLoop(ATree: TEnumerableTree);
var
  ANode: TNode;
begin
  for ANode in ATree do
    DoSomething(ANode);
end;

Поддержка перечислителя встроена в основные классы: TList, TStrings, TCollection, TComponent, ...

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

  IEnumerable = interface(IInterface)
    function GetEnumerator: IEnumerator;
  end;

Интерфейс IEnumerator определён следующим образом:

  IEnumerator = interface(IInterface)
    function GetCurrent: TObject;
    function MoveNext: Boolean;
    procedure Reset;
    property Current: TObject read GetCurrent;
  end;

Расширения FPC

Приведенные ниже примеры не поддерживаются Delphi, и предназначены только для FPC.

Перечисления и диапазоны

В Delphi невозможно использовать в цикле типы перечислений и диапазонов:

type
  TColor = (clRed, clBlue, clBlack);
  TRange = 'a'..'z';
var
  Color: TColor;
  ch: Char;
begin
  for Color in TColor do
    DoSomething(Color);
  for ch in TRange do
    DoSomethingOther(ch);
end.

Объявление перечислителей

В Delphi также невозможно добавить перечислитель не изменяя класс и добавить перечислитель к следующим типам не-классы/объекты/записи/интерфейсы. В FPC добавление перечислитиля для любого типа производится новым оператором operator Enumerator. Смотрите следующий пример:

type
  TMyRecord = record F1: Integer; F2: array of TMyType; end;
  TMyArrayEnumerator = class
    constructor Create(const A: TMyRecord);
    function Current: TMyType;
    function MoveNext: Boolean;
  end;

  // Это новый встроенный оператор
  operator Enumerator(const A: TMyRecord): TMyArrayEnumerator;
  begin
    Result := TMyArrayEnumerator.Create(A);
  end;

var
  A: MyRecord;
  V: TMyType
begin
  for V in A do
    DoSomething(V);
end.

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

type
  TUTF8StringEnumerator = class
  private
    FByteIndex: Integer;
    FCharIndex: Integer;
  public
    constructor Create(const A: UTF8String);
    function Current: UTF8Char;
    function MoveNext: Boolean;
  end;

  operator Enumerator(A: UTF8String): TUTF8StringEnumerator;
  begin
    Result := TUTF8String.Create(A);
  end;

var
  s: UTF8String;
  ch: UTF8Char;
  i: Integer;
begin
  // Здесь требуется выполнить O(N^2) операций
  for i := 1 to Length(s) do
    DoSomething(ch[i]);
  // А здесь, только O(N) операций
  for ch in s do
    DoSomething(ch);
end.

Использование имён идентификаторов отличных от MoveNext и Current

В Delphi вы должны использовать только функцию с именем 'MoveNext' и свойство с именем 'Current' для перечислений. В FPC можно использовать любые разрешенные имена. Для того, чтобы указать компилятору какая функция перечисляет элементы необходимо указать следующий модификатор 'enumerator MoveNext', а для свойства текущего элемента 'enumerator Current'. Смотрите следующий пример:

type
  { TMyListEnumerator }

  TMyListEnumerator = object
  private
    FCurrent: Integer;
  public
    constructor Create;
    destructor Destroy;
    function StepNext: Boolean; enumerator MoveNext;
    property Value: Integer read FCurrent; enumerator Current;
  end;

  TMyList = class
  end;

{ TMyListEnumerator }

constructor TMyListEnumerator.Create;
begin
  FCurrent := 0;
end;

destructor TMyListEnumerator.Destroy;
begin
  inherited;
end;

function TMyListEnumerator.StepNext: Boolean;
begin
  inc(FCurrent);
  Result := FCurrent <= 3;
end;

operator enumerator (AList: TMyList): TMyListEnumerator;
begin
  Result.Create;
end;

var
  List: TMyList;
  i: integer;
begin
  List := TMyList.Create;
  for i in List do
    WriteLn(i);
  List.Free;
end.

Предлагаемые расширения

Выбор перечисления для использования

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

type
  TTreeEnumeratorType = (tePreOrder, tePostOrder, teInOrder, teBreadthFirst)
procedure TraverseTree(Tree: TTree);
var
  Node: TNode;
begin
  // Вариант 1. Для экземляров класса мы можем вызвать метод Tree.GetEnumerator(teInOrder). 
  // Для классов мы можем вызвать метод класса
  for Node in Tree using GetEnumerator(teInOrder) do
    Dosomething(Node);

  // Вариант 2. Или мы можем вызвать глобальную функцию
  for Node in Tree using GetEnumerator(Tree, teInOrder) do
    Dosomething(Node);

  // Вариант 3. В предыдущем варианте 'in Tree' бесполезно, поэтому следующий код - это упрощенная форма:
  for Node using GetEnumerator(Tree, teInOrder) do
    Dosomething(Node);

  // Вариант 4. Мы можем попытаться избежать нового контекстного ключевого слова 'using', вызывая метод:
  for Node in Tree.GetSomeEnumerator(teInOrder) do
    Dosomething(Node);
  // но это принесет двусмысленность для компилятора, т.к. Tree.GetSomeEnumerator(teInOrder) может быть транслирован в
  // Tree.GetSomeEnumerator(teInOrder).GetEnumerator
  // Эта двусмысленность может быть разрешена проверкой, где класс применяет интерфейс IEnumerator
end;

// для базового типа мы вызовем только подходящую функцию
procedure TraverseString(S: String);
var
  C: Char;
begin
  for C in S using GetReverseStringEnumerator(S) do
    DoSomething(C);
end;

Получение Позиции перечисления, если возможно

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

type
  TUTF8StringEnumerator = class
  private
    FByteIndex: Integer;
    FCharIndex: Integer;
  public
    constructor Create(const A: UTF8String);
    function Current: UTF8Char;
    function CurrentIndex: Integer;
    function MoveNext: Boolean;
  end;

  operator GetEnumerator(A: UF8String): TUF8StringEnumerator;
  begin
    Result := TUF8String.Create(A);
  end;

var
  s: UF8String;
  ch: UF8Char;
  i: Integer;
begin

  // Неэффективный метод, как написано выше
  for i := 1 to Length(s) do
    Writeln(i, ': ', ch[i]);

  // Нормальный, но уродливый
  i := 1;
  for ch in s do begin
    Writeln(i, ': ', ch);
    Inc(i);
  end;

  // Предлагаемое расширение
  for ch in s index i do
    Writeln(i, ': ', ch);
end.

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

Ссылки