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) をインクリメントするだけであることを意味します。何らかの変更を行って初めて、実際のコピー (「複製」と呼ばれます) が作成されます。このトピックに関するより広範な紹介については、You're being lied toを参照してください。
PHP5
内部配列ポインタと HashPointer
PHP 5 の配列には、変更を適切にサポートする専用の「内部配列ポインター」(IAP) が 1 つあります。要素が削除されるたびに、IAP がこの要素を指しているかどうかがチェックされます。存在する場合は、代わりに次の要素に進みます。
foreach
は IAP を利用しますが、さらに複雑な問題があります。IAP は 1 つしかありませんが、1 つのアレイを複数のループの一部にすることができます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
に、現在の要素へのポインターとそのハッシュを foreach ごとにバックアップしますHashPointer
。ループ本体が実行された後、IAP はこの要素がまだ存在する場合に設定されます。ただし、要素が削除されている場合は、IAP が現在ある場所をそのまま使用します。このスキームはほとんどの場合、ある程度機能しますが、そこから抜け出すことができる多くの奇妙な動作があります。その一部を以下に示します。
配列の複製
IAP は配列の目に見える機能です (current
関数ファミリーを通じて公開されます)。IAP へのそのような変更は、コピー オン ライト セマンティクスの下での変更としてカウントされます。残念ながら、これforeach
は、多くの場合、反復処理中の配列を複製せざるを得ないことを意味します。正確な条件は次のとおりです。
- 配列は参照ではありません (is_ref=0)。それが参照である場合、それへの変更は伝播するはずなので、複製しないでください。
- 配列の refcount>1 です。が 1 の場合
refcount
、配列は共有されておらず、自由に直接変更できます。
配列が複製されていない場合 (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
ヒアのインクリメントは無害に聞こえますが、コピー オン ライト (COW) のセマンティクスに違反します: これは、refcount=2 配列の IAP を変更することを意味しますが、COW は変更が refcount= でのみ実行できることを指示します。 1 つの値。この違反により、(COW は通常は透過的ですが) ユーザーに見える動作の変更が発生します。これは、反復配列での IAP の変更が観察可能になるためです。ただし、配列での最初の非 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();
}
つまり、ループ本体が実行される前に、配列ポインターは既に前方に移動されています。これは、ループ本体が element で動作している間$i
に、IAP がすでにelement にあることを意味します$i+1
。unset
これが、反復中に変更を示すコード サンプルが常に現在の要素ではなく次の要素になる理由です。
例: テスト ケース
上記の 3 つの側面から、foreach
実装の特異性についてほぼ完全な印象が得られるはずです。次に、いくつかの例について説明します。
この時点で、テスト ケースの動作は簡単に説明できます。
テスト ケース 1 と 2$array
は refcount=1 で始まるため、 によって複製されることはありませんforeach
: のみrefcount
がインクリメントされます。その後、ループ本体が配列 (その時点で refcount=2 を持つ) を変更すると、その時点で重複が発生します。Foreach は、変更されていない のコピーで作業を続けます$array
。
テスト ケース 3 では、配列が複製されていないため、変数foreach
の IAP が変更されます。$array
反復の最後に、IAP は NULL (反復が完了したことを意味します) であり、これeach
は を返すことによって示されfalse
ます。
テスト ケース 4 と 5 では、each
とreset
の両方が参照関数です。には渡されるときに があるため、複製する必要があります$array
。refcount=2
そのためforeach
、別の配列で再び作業します。
例: current
in foreachの効果
さまざまな複製の動作を示す良い方法は、ループcurrent()
内の関数の動作を観察すること です。foreach
次の例を検討してください。
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
current()
ここでは、配列を変更しませんが、それが参照参照関数 (実際には、prefer-ref) であることを知っておく必要があります。これは、すべて参照による他のすべての関数とうまく連携するために必要です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()
しかし、これが参照であるため、by-ref関数に渡すときに配列を複製する必要はなくなりました。したがって、同じ配列で作業しますcurrent()
。ただし、がポインターを進めるforeach
方法により、1 つずれる動作が引き続き表示されます。foreach
参照による反復を行う場合、同じ動作が得られます。
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 */
ここでは、ループが開始されたときの refcount$array
は 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'
、アロケータがたまたま同じメモリ位置を再利用して要素を格納するということです。したがって、 foreach は、新しく挿入された要素に直接ジャンプすることになり、ループをショートカットします。
ループ中の反復エンティティの置換
最後に言及したい奇妙なケースの 1 つは、PHP では、ループ中に反復されたエンティティーを置き換えることができるということです。したがって、1 つの配列で反復処理を開始し、途中で別の配列に置き換えることができます。または、配列の反復処理を開始してから、オブジェクトに置き換えます。
$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 は他のエンティティを最初から反復し始めます。
PHP7
ハッシュテーブル イテレータ
覚えていれば、配列反復の主な問題は反復途中の要素の削除を処理する方法でした。PHP 5 は、この目的のために単一の内部配列ポインター (IAP) を使用していましたが、これはやや最適ではありませんでした。1 つの配列ポインターを拡張して、複数の foreach ループをreset()
同時に実行し、その上でその他の操作をサポートする必要があったからです。
PHP 7 は別のアプローチを使用します。つまり、任意の量の外部の安全なハッシュテーブル イテレーターの作成をサポートします。これらの反復子は配列に登録する必要があり、その時点から IAP と同じセマンティクスになります。配列要素が削除されると、その要素を指すすべてのハッシュテーブル反復子が次の要素に進みます。
これは、foreach
が IAPをまったく使用しないことを意味します。foreach
ループは etc の結果にまったく影響を与えず、ループ自体の動作はetccurrent()
のような関数によって影響を受けることはありません 。reset()
配列の複製
PHP 5 と PHP 7 の間のもう 1 つの重要な変更点は、配列の複製に関するものです。IAP が使用されなくなったため、値による配列反復はrefcount
、すべての場合で (配列の複製ではなく) 増分のみを行います。ループ中に配列が変更された場合、foreach
その時点で重複が発生し (コピーオンライトに従って) 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 は同じままです。元のアレイを使用しながら、IAP を変更する前each()
にreset()
アレイを複製します。foreach
(アレイが共有されていたとしても、IAP の変更は問題ではありませんでした。)
current()
2 番目の例は、さまざまな構成での の動作に関連していました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 を介した両方のループのクロスコンタミネーションがなくなったためです。
現在修正されているもう 1 つの奇妙なエッジ ケースは、たまたま同じハッシュを持つ要素を削除および追加したときに得られる奇妙な効果です。
$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 復元メカニズムが新しい要素にジャンプしていました。これは、削除された要素と同じように「見えた」ためです (ハッシュとポインターが衝突するため)。要素ハッシュに依存しなくなったため、これはもはや問題ではありません。