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