for-in loop/ja

From Lazarus wiki
Jump to navigationJump to search
日本語版メニュー
メインページ - Lazarus Documentation日本語版 - 翻訳ノート - 日本語障害情報

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

これは集合に対して繰り返しを行うが、基本的な forループは数値によるインデクス・カウンタを用いるのに対して、for-inループは代わりに、直接使用されるカウンタ変数に集合要素を代入する。For-inは繰り返しが必要とされる文字列、配列、セットおよび他の任意の集合で有効である。空の集合に対するループは何もしない。カウンタ変数はループの中で変更できない。

For-in loop構文はDelphi 2005以降で導入された。FPC2.4.2で実装された。

公式なドキュメントはここである: Reference guide chapter 13

DelphiとFPCでの実装

For inループの文法は以下である:

文字列ループ

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;

ループ変数はコンテナ値の一時的コピーである

ループ変数は for-inループが実行されるコンテナ内に保存されている値のコピーである

program tempcopy;

{$IFDEF FPC}
  {$mode Delphi}
{$ENDIF}

uses SysUtils;

type
  PointerAddress = NativeUInt;

var
  pIntArr: Pointer;
  IntArr: Array of Integer;
  
procedure ArrayLoop(const AArr: Array of Integer);
var
  i: Integer;
begin
  pIntArr := @AArr;
  writeln('Argument: ', PointerAddress(pIntArr)); // <-- 最初の要素と同じメモリアドレス
  pIntArr := @i;
  writeln('Local loop variable: ', PointerAddress(pIntArr)); // <-- ローカルな一時変数 i
  for i in AArr do
  begin
    pIntArr := @i;
    writeln('Loop variable: ', PointerAddress(pIntArr)); // <-- ローカルな一時変数 i
  end;
end;

begin
  // 3つの要素を配列に格納
  SetLength(IntArr, 3);

  pIntArr := @pIntArr;
  writeln('global pIntArr address: ', PointerAddress(pIntArr));

  // IntArrは最初の要素に対するポインタとは独立している
  pIntArr := @IntArr;
  writeln('global IntArr address: ', PointerAddress(pIntArr));
  pIntArr := @IntArr;
  writeln('IntArr points to: ', PointerAddress(pIntArr^));

  // 1つの要素に対するアドレス
  pIntArr := @IntArr[0];
  writeln('Item 1: ', PointerAddress(pIntArr)); // <-- 最初の要素のメモリアドレス
  pIntArr := @IntArr[1];
  writeln('Item 2: ', PointerAddress(pIntArr));
  pIntArr := @IntArr[2];
  writeln('Item 3: ', PointerAddress(pIntArr));

  writeln('array loop');
  ArrayLoop(IntArr);
end.

コンテナの走査

コンテナクラスを走査するには加算子(enumerator)が必要である。加算子は以下のテンプレートによればクラス構造である:

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

加算子クラスに必要なのは以下の2つ: 加算子に次を読み込ませる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
  // Treeから次のノードを取得するロジック
  if FCurrent = nil then
    FCurrent := FTree.GetFirstNode
  else
    FCurrent := FTree.GetNextNode(FCurrent);
  Result := FCurrent <> nil;
end;

function TEnumerableTree.GetEnumerator: TTreeEnumerator;
begin
  Result := TTreeEnumerator.Create(Self);
  // 注意: このResultはループの後にコンパイラによって自動的に解放される。
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;

1つのクラスに対する複数の加算子

クラス、オブジェクト、レコードに加算子を加えることができる。以下は逆順でTEnumerableTreeを走査する加算子の例である:

type
  TEnumerableTree = class;

  TTreeEnumerator = class
  ...正順で走査する、上記参照のこと...
  end;

  TTreeReverseEnumerator = class
  private
    FTree: TEnumerableTree;
    FCurrent: TNode;
  public
    constructor Create(ATree: TEnumerableTree); 
    function MoveNext: Boolean;
    property Current: TNode read FCurrent;
    function GetEnumerator: TTreeReverseEnumerator; // それ自身を返す
  end;

  TEnumerableTree = class
  public
    function GetEnumerator: TTreeEnumerator;
    function GetReverseEnumerator: TTreeReverseEnumerator;
  end;

...TTreeEnumeratorの実装部は上記参照のこと...

constructor TTreeReverseEnumerator.Create(ATree: TEnumerableTree);
begin
  inherited Create;
  FTree := ATree;
end;

function TTreeReverseEnumerator.MoveNext: Boolean;
begin
  // 次のノードをツリーから逆順で得るロジック
  if FCurrent = nil then
    FCurrent := FTree.GetLastNode
  else
    FCurrent := FTree.GetPrevNode(FCurrent);
  Result := FCurrent <> nil;
end;

function TTreeReverseEnumerator.GetEnumerator: TTreeReverseEnumerator;
begin
  Result := Self;
end;

function TEnumerableTree.GetReverseEnumerator: TTreeReverseEnumerator;
begin
  Result := TTreeReverseEnumerator.Create(Self);
  // 注意: このResultはループの後にコンパイラによって自動的に解放される。
end;

この後、以下のコードが実行できる:

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

FPCでの拡張

次のコード例はFPCでのみ実装されており、Delphiではサポートされていない構文である。

加算子とサブレンジ型の走査

Delphiでは、加算された型および部分範囲型を走査することのどちらも不可能であるが、Free Pascalでは以下のように書ける:

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.

これは、ハードキャストが必ずしも加算値の部分ではない値に用いられるため型システムが無効にされるという、バグレポート0029147から取った以下の例で、さらに示すことができる:

type
  TSomeEnums = (One, Two, Three);

resourcestring
  SOne = 'One ape';
  STwo = 'Two apes';
  SThree = 'Three apes';

const
  SSomeEnumStrings: array [Low(TSomeEnums)..High(TSomeEnums)] of string = (
    SOne, STwo, SThree);

var
  i: Integer;
  SE: TSomeEnums;

begin
  for i := 0 to 4 do begin
    SE := TSomeEnums(i);  // ハードキャスト。2以上になりうるが、型システムは今無効である。
    WriteLn(SSomeEnumStrings[SE]);
  end;
end.

このプログラマは自身が最善を知っていると仮定して、コンパイラに i が加算型あり、コンパイラがさらにチェックを行わないだろうと命令している。

もちろんこのプログラマは誤っており、コンパイラはそうしないだけの分別はある...

For in doで加算型を走査するにより、ハードキャストは余計であり、コードは型について安全である:

type
  TSomeEnums = (One, Two, Three);

resourcestring
  SOne   = 'One ape';
  STwo   = 'Two apes';
  SThree = 'Three apes';

const
  SSomeEnumStrings: array [Low(TSomeEnums)..High(TSomeEnums)] of string = (
    SOne, STwo, SThree);

var
  SE: TSomeEnums;
begin
    for SE in TSomeEnums do
      WriteLn(SSomeEnumStrings[SE]);
end.

加算子を宣言する

また、Delphiでもクラスを変更することなしに加算子を加えること、非クラス/オブジェクト/レコード/インターフェースに加算子を加えることのいずれも不可能である。

FPCは、以下の例のように新しい文法、operator型、Enumeratorを用いることでこれを可能としている:

type
  TMyRecord = record F1: Integer; F2: array of TMyType; end;
  TMyArrayEnumerator = class
  private
    function GetCurrent: TMyType;
  public
    constructor Create(const A: TMyRecord);
    property Current: TMyType read GetCurrent;
    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文字列を走査する

特に有用な例として、上記の拡張はUTF-8文字列を走査するうえで非常に効率が良い:

uses
  LazUTF8;
interface
type
  { TUTF8StringAsStringEnumerator
    UTF-8文字の正確なエンコーディングを知る、あるいはコードの中の今の文字列定数を
    使いたいときに有用である。
    安全性の観点より、代わりにコードポイント値(基数)を使うべきである。
    もし速度を重視するときは、加算子を用いてはいけない。代わりにMoveNextメソッドで
    示されているようにPCharを直接用い、UTF-8を読むこと。これはいくつかの興味深い特
    徴である。}

  TUTF8StringAsStringEnumerator = class
  private
    fCurrent: UTF8String;
    fCurrentPos, fEndPos: PChar;
    function GetCurrent: UTF8String;
  public
    constructor Create(const A: UTF8String);
    property Current: UTF8String read GetCurrent;
    function MoveNext: Boolean;
  end;

  operator Enumerator(A: UTF8String): TUTF8StringAsStringEnumerator;

var
  Form1: TForm1;

implementation

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

{ TUTF8StringAsStringEnumerator }

function TUTF8StringAsStringEnumerator.GetCurrent: UTF8String;
begin
  Result:=fCurrent;
end;

constructor TUTF8StringAsStringEnumerator.Create(const A: UTF8String);
begin
  fCurrentPos:=PChar(A); // 注意: もしA=''ならばPChar(A)は#0文字列に対するポインタを返す
  fEndPos:=fCurrentPos+length(A);
end;

function TUTF8StringAsStringEnumerator.MoveNext: Boolean;
var
  l: Integer;
begin
  if fCurrentPos<fEndPos then
  begin
    l:=UTF8CharacterLength(fCurrentPos);
    SetLength(fCurrent,l);
    Move(fCurrentPos^,fCurrent[1],l);
    inc(fCurrentPos,l);
    Result:=true;
  end else
    Result:=false;
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  s, ch: UTF8String;
  i: SizeInt;
begin
  s:='mäßig';

  // このようにUTF8LengthとUTF8Copyを用いるのは遅い、O(n)^2を要する
  for i:=1 to UTF8Length(s) do
    writeln('ch=',UTF8Copy(s,i,1));

  // 上記の加算子を用いるることは手短で極めて速い、O(n)を要する
  for ch in s do
    writeln('ch=',ch);
end;

組込のMoveNextとCurrentの代わりにどのような定義子も用いる

Delphiでは加算子の中でMoveNextという名とCurrentという名の関数を適切に用いなければならない。 FPCでは望む名前が何であれ用いることができる。これは文法、'enumerator MoveNext;'と'enumerator Current;'においてenumerator修飾子の使用で可能となっている。以下の例にあるように:

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
  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: UTF8String): TUTF8StringEnumerator;
  begin
    Result := TUTF8String.Create(A);
  end;

var
  s: UTF8String;
  ch: UTF8Char;
  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);

  // 逆方向に走査するために提案された拡張(downtoと同等)
  for ch in reverse s do
    Writeln(i, ': ', ch);

  // 提案されたインデクスの拡張
  for ch in reverse s index i do
    Writeln(i, ': ', ch);
end.

インデクスは任意の型を返すように設計されている(即ち、整数でなくてもよい)。例えばツリーの走査の場合、インデクスは現在のルートからノードまでのパスを記述した配列を返すかもしれない。

参考