foreach
3つの異なる種類の値の反復をサポートします。
以下では、さまざまなケースでイテレーションがどのように機能するかを正確に説明します。最も単純なケースはTraversable
オブジェクトです。これらforeach
は基本的に、これらの行に沿ったコードの構文糖衣です。
foreach ($it as $k => $v) { /* ... */ }
/* translates to: */
if ($it instanceof IteratorAggregate) {
$it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
$v = $it->current();
$k = $it->key();
/* ... */
}
内部クラスの場合、基本的Iterator
にCレベルのインターフェースをミラーリングする内部APIを使用することにより、実際のメソッド呼び出しが回避されます。
配列とプレーンオブジェクトの反復は、かなり複雑です。まず第一に、PHPでは「配列」は実際に順序付けされた辞書であり、これらはこの順序に従ってトラバースされます(次のようなものを使用しない限り、挿入順序と一致します)。sort
)。これは、キーの自然な順序による反復(他の言語のリストがよく機能する方法)またはまったく定義されていない順序(他の言語の辞書がよく機能する方法)による反復とは対照的です。
同じことがオブジェクトにも当てはまります。オブジェクトのプロパティは、プロパティ名をその値にマッピングする別の(順序付けされた)辞書として表示され、いくつかの可視性の処理も行われるためです。ほとんどの場合、オブジェクトのプロパティは実際にはこのような非効率的な方法で格納されていません。ただし、オブジェクトの反復処理を開始すると、通常使用されるパックされた表現が実際の辞書に変換されます。その時点で、プレーンオブジェクトの反復は配列の反復と非常によく似たものになります(そのため、ここではプレーンオブジェクトの反復についてはあまり説明しません)。
ここまでは順調ですね。辞書を反復するのは難しいことではありませんよね?問題は、配列/オブジェクトが反復中に変更される可能性があることに気づいたときに始まります。これが発生する可能性のある方法はいくつかあります。
- あなたが使用して参照することによって反復した場合
foreach ($arr as &$v)
、その後$arr
の参照になっていると、あなたは反復中にそれを変更することができます。
- PHP 5では、値で反復しても同じことが当てはまりますが、配列は事前に参照でした。
$ref =& $arr; foreach ($ref as $v)
- オブジェクトにはハンドル渡しのセマンティクスがあり、ほとんどの実用的な目的では、参照のように動作します。したがって、オブジェクトは反復中に常に変更できます。
反復中に変更を許可することに関する問題は、現在使用している要素が削除される場合です。現在の配列要素を追跡するためにポインターを使用するとします。この要素が解放されると、宙ぶらりんのポインタが残ります(通常はsegfaultになります)。
この問題を解決するにはさまざまな方法があります。PHP 5とPHP 7はこの点で大きく異なります。両方の動作について以下で説明します。要約すると、PHP 5のアプローチはかなり馬鹿げており、あらゆる種類の奇妙なエッジケースの問題を引き起こしますが、PHP 7のより複雑なアプローチは、より予測可能で一貫した動作をもたらします。
最後の予備として、PHPは参照カウントとコピーオンライトを使用してメモリを管理することに注意してください。つまり、値を「コピー」した場合、実際には古い値を再利用し、その参照カウント(refcount)をインクリメントするだけです。ある種の変更を実行すると、実際のコピー(「複製」と呼ばれます)が実行されます。このトピックのより広範な紹介については、嘘をついているを参照してください。
PHP 5
内部配列ポインターとHashPointer
PHP 5の配列には、変更を適切にサポートする1つの専用「内部配列ポインター」(IAP)があります。要素が削除されるたびに、IAPがこの要素を指しているかどうかのチェックが行われます。ある場合は、代わりに次の要素に進みます。
一方でforeach
唯一のIAPがありますが、1つのアレイは、複数の一部にすることができます:IAPのメイク使用を行い、そこに追加の合併症であるforeach
ループ:
// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
foreach ($arr as &$v) {
// ...
}
}
内部配列ポインターが1つだけの2つの同時ループをサポートするにforeach
は、次の処理を実行します。ループ本体が実行される前foreach
に、現在の要素へのポインターとそのハッシュをfor-eachごとにバックアップしますHashPointer
。ループ本体の実行後、IAPはまだ存在する場合、この要素に設定されます。ただし、要素が削除されている場合は、IAPが現在ある場所で使用します。このスキームは主にちょっとした種類の作品ですが、あなたがそれから得ることができる多くの奇妙な振る舞いがあります。そのうちのいくつかを以下に示します。
アレイの複製
IAPは、(current
関数のファミリを通じて公開される)配列の可視機能です。IAPに対するこのような変更は、コピーオンライトセマンティクスでの変更としてカウントされます。残念ながら、これforeach
は多くの場合、反復している配列の複製を余儀なくされることを意味します。正確な条件は次のとおりです。
- 配列は参照ではありません(is_ref = 0)。参照の場合は、変更が反映されるはずなので、複製しないでください。
- 配列のrefcount> 1です。
refcount
が1の場合、配列は共有されず、直接自由に変更できます。
配列が複製されていない場合(is_ref = 0、refcount = 1)、その配列のみrefcount
が増分されます(*)。さらに、foreach
参照渡しを使用すると、(重複する可能性のある)配列が参照に変わります。
重複が発生する例として、このコードを検討してください。
function iterate($arr) {
foreach ($arr as $v) {}
}
$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);
ここで$arr
は、IAPの変更がに$arr
リークするのを防ぐために複製され$outerArr
ます。上記の条件に関して、配列は参照ではなく(is_ref = 0)、2つの場所で使用されます(refcount = 2)。この要件は残念であり、次善の実装の成果物です(ここでは反復中に変更の心配はないため、最初にIAPを使用する必要はありません)。
(*)refcount
hereのインクリメントは無害に聞こえますが、copy-on-write(COW)セマンティクスに違反しています:これは、refcount = 2配列のIAPを変更しようとしているのに対し、COWは変更がrefcount =でのみ実行できることを示しています1つの値。反復配列のIAP変更は観測可能であるため、この違反はユーザーに見える動作の変化(COWは通常は透過的です)をもたらしますが、配列の最初の非IAP変更までのみです。代わりに、3つの「有効な」オプションは、a)常に複製すること、b)をインクリメントしないrefcount
こと、したがって反復配列をループで任意に変更できるようにすること、またはc)IAPをまったく使用しないこと(PHP) 7ソリューション)。
ポジション昇順
以下のコードサンプルを正しく理解するために注意する必要がある最後の実装の詳細が1つあります。データ構造をループする「通常の」方法は、疑似コードでは次のようになります。
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
code();
move_forward(arr);
}
ただしforeach
、かなり特殊なスノーフレークであるため、少し異なる方法で実行することを選択します。
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
move_forward(arr);
code();
}
つまり、ループボディが実行される前に、配列ポインターが既に前方に移動されています。これは、ループ本体が要素$i
で動作している間、IAPはすでに要素にあることを意味します$i+1
。これは、コードサンプルは、反復処理中に変形例を示す理由では常に次の要素ではなく、現在の1。unset
例:テストケース
上記の3つの側面は、foreach
実装の特異性のほぼ完全な印象を与えるはずです。次に、いくつかの例について説明します。
この時点では、テストケースの動作を簡単に説明できます。
テストケース1及び2において$array
参照カウント= 1から始まり、それはによって複製されることはありませんforeach
のみ:refcount
インクリメントされます。その後、ループ本体が配列(その時点でrefcount = 2を持つ)を変更すると、その時点で重複が発生します。Foreachは、変更されていないのコピーで引き続き作業します$array
。
テストケース3では、配列が重複していないためforeach
、$array
変数のIAPが変更されます。反復の終了時に、IAPはNULL(反復が完了したことを意味します)であり、をeach
返すことで示しますfalse
。
4テストケースおよび5の両方each
とreset
することにより、基準関数です。$array
持っているrefcount=2
、それが重複する必要があるので、それは、彼らに渡されたとき。そのためforeach
、別のアレイで再び動作します。
例:current
foreachの影響
さまざまな複製動作を示す良い方法はcurrent()
、foreach
ループ内の関数の動作を観察すること です。この例を考えてみましょう:
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
ここでcurrent()
は、配列を変更しなくても、それが参照関数(実際には優先参照)であることを知っておく必要があります。これは、他のすべての関数next
(参考文献など)とうまく機能するために必要です。参照による通過アレイを分離する必要があり、従ってことを意味する$array
とforeach-array
異なるであろう。の2
代わりに取得する理由1
は、上記でも説明されています。ユーザーコードを実行する前ではなく、実行する前にforeach
配列ポインターを進めます。したがって、コードは最初の要素にありますが、すでにポインタを2番目の要素に進めています。foreach
ここで、小さな変更を試してみましょう。
$ref = &$array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
ここにはis_ref = 1のケースがあるため、配列はコピーされません(上記と同様)。しかし、これが参照であるため、参照渡しcurrent()
関数に渡すときに配列を複製する必要がなくなりました。このようにcurrent()
してforeach
、同じアレイ上の作品。ただし、foreach
ポインタを進める方法が原因で、1つずれた動作が表示されます。
参照による反復を実行すると、同じ動作が得られます。
foreach ($array as &$val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
ここで重要なのは、$array
参照によって反復されるときにforeachがis_ref = 1 を作成するため、基本的には上記と同じ状況になります。
別の小さなバリエーション、今回は配列を別の変数に割り当てます。
$foo = $array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 1 1 1 1 1 */
ここでは$array
、ループの開始時のrefcount は2であるため、一度複製を前もって実行する必要があります。したがって$array
、foreachで使用される配列は、最初から完全に分離されます。そのため、ループの前のどこにでもIAPの位置を取得します(この場合は、最初の位置にあります)。
例:反復中の変更
反復中に変更を考慮に入れることは、すべてのforeachの問題が発生した場所なので、このケースのいくつかの例を検討するのに役立ちます。
同じ配列上でこれらのネストされたループを検討してください(参照による反復が実際に同じであることを確認するために使用されている場合):
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Output: (1, 1) (1, 3) (1, 4) (1, 5)
ここで予想される部分は、(1, 2)
要素1
が削除されたために出力から欠落している部分です。おそらく予期しないことは、最初の要素の後で外側のループが停止することです。何故ですか?
この背後にある理由は、上記の入れ子ループハックです。ループ本体が実行される前に、現在のIAP位置とハッシュがにバックアップされますHashPointer
。ループ本体の後は、要素がまだ存在する場合にのみ復元されます。それ以外の場合は、現在のIAP位置(何であれ)が代わりに使用されます。上記の例では、これがまさに当てはまります。外側のループの現在の要素は削除されているため、内側のループによって既に終了としてマークされているIAPが使用されます。
HashPointer
バックアップ+復元メカニズムのもう1つの結果は、IAPへの変更reset()
などによる影響が通常ないことforeach
です。たとえば、次のコードは、reset()
存在しないかのように実行されます。
$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// output: 1, 2, 3, 4, 5
その理由はreset()
、IAP を一時的に変更している間、ループ本体の後で現在のforeach要素に復元されるためです。reset()
ループに強制的に影響を与えるには、現在の要素をさらに削除して、バックアップ/復元メカニズムが失敗するようにする必要があります。
$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
var_dump($value);
unset($array[1]);
reset($array);
}
// output: 1, 1, 3, 4, 5
しかし、それらの例はまだ正気です。HashPointer
復元が要素へのポインタとそのハッシュを使用して、それがまだ存在するかどうかを判断することを覚えている場合、本当に楽しいことが始まります。しかし:ハッシュには衝突があり、ポインターは再利用できます!つまり、配列キーを注意深く選択することで、foreach
削除された要素がまだ存在していると信じることができるため、直接要素にジャンプします。例:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
reset($array);
var_dump($value);
}
// output: 1, 4
ここでは、通常1, 1, 3, 4
、前のルールに従って出力を期待する必要があります。'FYFY'
削除されたelementと同じハッシュを持つものがどうなるか'EzFY'
、アロケーターはたまたま同じメモリー位置を再利用してelementを格納します。そのため、foreachは、新しく挿入された要素に直接ジャンプすることになり、ループが短縮されます。
ループ中に反復エンティティを置換する
私が言及したい最後の奇妙なケースの1つは、ループ中にPHPを使用して反復エンティティを置換できることです。したがって、あるアレイで反復を開始し、途中で別のアレイと置き換えることができます。または、配列の反復を開始し、それをオブジェクトに置き換えます。
$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];
$ref =& $arr;
foreach ($ref as $val) {
echo "$val\n";
if ($val == 3) {
$ref = $obj;
}
}
/* Output: 1 2 3 6 7 8 9 10 */
この例でわかるように、置換が発生すると、PHPは最初から他のエンティティの反復を開始します。
PHP 7
ハッシュテーブル反復子
まだ覚えているかもしれませんが、配列反復の主な問題は、反復中の要素の削除をどのように処理するかでした。PHP 5はこの目的で単一の内部配列ポインター(IAP)を使用しましたが、複数の同時foreachループとreset()
その上での相互作用などをサポートするために1つの配列ポインターを拡張する必要があったため、これはやや最適ではありませんでした。
PHP 7は別のアプローチを使用しています。つまり、任意の量の外部の安全なハッシュテーブル反復子の作成をサポートしています。これらのイテレータは配列に登録する必要があり、その時点からIAPと同じセマンティクスを持ちます。配列要素が削除されると、その要素を指すすべてのハッシュテーブルイテレータは次の要素に進みます。
つまり、foreach
はIAP をまったく使用しなくなります。foreach
ループは、結果current()
などにまったく影響を与えません。ループ自体の動作は、関数reset()
などの影響を受けません 。
アレイの複製
PHP 5とPHP 7の間のもう1つの重要な変更は、配列の複製に関するものです。IAPが使用されなくなったため、値による配列の反復はrefcount
、すべての場合に(配列を複製する代わりに)増分のみを実行します。foreach
ループ中に配列が変更されると、その時点で重複が発生し(copy-on-writeに応じて)foreach
、古い配列での作業が続行されます。
ほとんどの場合、この変更は透過的であり、パフォーマンスの向上以外の効果はありません。ただし、異なる動作が発生する場合が1つあります。つまり、配列が事前に参照であった場合です。
$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */
以前は、参照配列の値による反復は特殊なケースでした。この場合、重複は発生しなかったため、反復中の配列のすべての変更はループによって反映されます。PHP 7では、この特別なケースはなくなりました。配列の値による反復は、ループ中の変更を無視して、常に元の要素で動作し続けます。
もちろん、これは参照による反復には適用されません。参照によって反復する場合、すべての変更はループによって反映されます。興味深いことに、プレーンオブジェクトの値による繰り返しについても同じことが言えます。
$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
var_dump($val);
$obj->bar = 42;
}
/* Old and new output: 1, 42 */
これは、オブジェクトのハンドルによるセマンティクスを反映しています(つまり、値によるコンテキストでも参照のように動作します)。
例
テストケースから始めて、いくつかの例を考えてみましょう:
テストケース1と2は同じ出力を保持します。値による配列の反復は、常に元の要素で機能し続けます。(この場合、refcounting
複製の動作はPHP 5とPHP 7でまったく同じです)。
テストケース3の変更:Foreach
IAPを使用しeach()
ないため、ループの影響を受けません。前後で同じ出力になります。
テストケース4及び5ステーに同じeach()
とreset()
しつつ、IAPを変更する前に、アレイを複製しますforeach
依然として元の配列を使用します。(配列が共有されている場合でも、IAPの変更が問題になることはありません。)
2番目の例のセットはcurrent()
、さまざまreference/refcounting
な構成でのの動作に関連しています。current()
ループの影響をまったく受けないため、これはもはや意味がありません。そのため、戻り値は常に同じままです。
ただし、反復中の変更を検討すると、興味深い変更がいくつかあります。新しい行動を正気に見つけていただければ幸いです。最初の例:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
// (3, 1) (3, 3) (3, 4) (3, 5)
// (4, 1) (4, 3) (4, 4) (4, 5)
// (5, 1) (5, 3) (5, 4) (5, 5)
ご覧のとおり、最初の反復後に外部ループが中止されなくなりました。その理由は、両方のループに完全に別個のハッシュテーブルイテレータがあり、共有IAPを介した両方のループの相互汚染がないためです。
現在修正されている別の奇妙なエッジのケースは、たまたま同じハッシュを持つ要素を削除して追加したときに得られる奇妙な影響です。
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4
以前は、HashPointerの復元メカニズムは、削除された要素と同じように「見えた」ため(ハッシュとポインタの衝突により)、新しい要素に直接ジャンプしました。要素のハッシュに依存しなくなったため、これは問題ではなくなりました。