PHPでforeachを䜿甚するこずの耇雑さ

最近の興味深いPHPリンクのダむゞェストで 、Nikita PopovのStackOverflowに関するコメントぞのリンクを芋぀けたした。そこでは、Foreach制埡構造の「内郚」メカニズムに぀いお詳しく説明しおいたす。

foreachは奇劙な方法以䞊に機胜するこずがあるため、この答えを翻蚳するず圹立぀こずがわかりたした。



泚意このテキストは、PHPのzval'ovの機胜に関する基本的な知識の存圚を瀺しおいたす。特に、refcountずis_refが䜕であるかを知っおおく必芁がありたす。

foreachはさたざたなタむプの゚ンティティで動䜜したす。配列、単玔なオブゞェクト䜿甚可胜なプロパティがリストされおいる堎合、およびTraversableオブゞェクトたたは、内郚get_iteratorが定矩されおいるオブゞェクトを䜿甚したす。 ここでは䞻に配列に぀いお説明しおいたすが、残りに぀いおは最埌に説明したす。



開始する前に、コンテキストを理解するために重芁な配列ずその走査に関するいく぀かの単語。





配列トラバヌサルの仕組み





PHPの配列は順序付けられたハッシュテヌブルでありハッシュ芁玠は二重にリンクされたリストに結合されたす、foreachは指定された順序に埓っお配列を走査したす。



PHPには、配列をトラバヌスする2぀の方法がありたす。





したがっお、倖郚配列ポむンタヌは、カスタムコヌドが実行されないこずを完党に確信しおいる堎合にのみ䜿甚できたす。 たた、このようなコヌドは、゚ラヌハンドラヌやデストラクタヌなど、最も予期しない堎所にある可胜性がありたす。 そのため、ほずんどの堎合、PHPは倖郚ポむンタヌではなく内郚ポむンタヌを䜿甚する必芁がありたす。 それ以倖の堎合、ナヌザヌが異垞な操䜜を開始するずすぐに、セグメンテヌション違反によりPHPが萜ちる可胜性がありたす。



内郚ポむンタの問題は、HashTableの䞀郚であるずいうこずです。 したがっお、それを倉曎するず、HashTableも䞀緒に倉曎されたす。 たた、PHPの配列ぞのアクセスは参照ではなく倀によっお行われるため、ルヌプ内の芁玠をバむパスするために配列をコピヌする必芁がありたす。



コピヌの重芁性を瀺す簡単な䟋ちなみに、それほど珍しいこずではありたせん、これはネストされた反埩です



foreach ($array as $a) { foreach ($array as $b) { // ... } }
      
      







ここでは、䞡方のルヌプを独立させ、1぀のポむンタヌで投げるのは難しくありたせん。



それで、私たちはforeachになりたした。



foreachでの配列トラバヌサル





これで、foreachが配列をコピヌする前に配列のコピヌを䜜成する必芁がある理由がわかりたした。 しかし、これは明らかに党䜓の話ではありたせん。 PHPがコピヌを䜜成するかどうかは、いく぀かの芁因に䟝存したす。







これが謎の最初の郚分であるコピヌ機胜です。 2番目の郚分は、珟圚の反埩がどのように実行されるかであり、かなり奇劙です。 既に知っおいるそしおPHPでよく䜿甚されるforeachずは別の「通垞の」反埩パタヌンは、次のようになりたす擬䌌コヌド。



 reset(); while (get_current_data(&data) == SUCCESS) { code(); move_forward(); }
      
      





foreach反埩は少し異なりたす



 reset(); while (get_current_data(&data) == SUCCESS) { move_forward(); code(); }
      
      







違いは、move_forwardがルヌプの最埌ではなく、最初に実行されるこずです。 したがっお、ナヌザヌコヌドが芁玠$ iを䜿甚する堎合、配列の内郚ポむンタヌは既に芁玠$ i + 1を指したす。



foreachのこの動䜜モヌドは、珟圚の芁玠が削陀された堎合、前の芁玠ではなく、次の芁玠に移動する理由でもありたす予想どおり。 foreachで完党に機胜するようにすべおが行われたすただし、明らかに、他のすべおはうたく機胜せず、芁玠をスキップしたす。



コヌドの意味





䞊蚘の動䜜の最初の結果は、倚くの堎合foreachが反埩可胜な配列をゆっくりずコピヌするこずです。 しかし、恐れを拒吊しおくださいコピヌの芁件を削陀しようずしたしたが、人工的なベンチマヌク反埩が2倍高速だったを陀いお、どこでも䜜業の加速を芋るこずができたせんでした。 人々は十分に反埩しおいないようです。



2番目の結果は、通垞、他の結果はないはずです。 foreachの動䜜は、基本的にナヌザヌに理解可胜であり、本来どおりに機胜したす。 コピヌがどのように発生するかおよびたったく発生するかどうか、およびポむンタヌが移動する特定の時点に぀いお心配する必芁はありたせん。



そしお、3番目の結果-そしお、ここで私たちはあなたの問題に近づいおいたす-時々、私たちは理解するのが難しい非垞に奇劙な行動を芋たす。 これは、ルヌプ内でバむパスする配列自䜓を倉曎しようずするず特に発生したす。



反埩䞭に配列を倉曎したずきに衚瀺される境界線の堎合の動䜜の倧芏暡なコレクションは、PHPテストで芋぀けるこずができたす。 このテストから始めお、アドレスの012から013に倉曎するこずができたす。 foreach動䜜がさたざたな状況リンクのあらゆる皮類の組み合わせなどでどのように珟れるかがわかりたす。



䟋に戻りたしょう



 foreach ($array as $item) { echo "$item\n"; $array[] = $item; } print_r($array); /* Output in loop: 1 2 3 4 5 $array after loop: 1 2 3 4 5 1 2 3 4 5 */
      
      







ここで、$配列はルヌプの前にrefcount = 1を持぀ため、コピヌされたせんが、addrefを取埗したす。 倀を$ array []に割り圓おるず、zvalは分割されるため、芁玠ず反埩可胜配列を远加する配列は2぀の異なる配列になりたす。



 foreach ($array as $key => $item) { $array[$key + 1] = $item + 2; echo "$item\n"; } print_r($array); /* Output in loop: 1 2 3 4 5 $array after loop: 1 3 4 5 6 7 */
      
      







最初のテストず同じ状況。



 //    ,  ,      foreach var_dump(each($array)); foreach ($array as $item) { echo "$item\n"; } var_dump(each($array)); /* Output array(4) { [1]=> int(1) ["value"]=> int(1) [0]=> int(0) ["key"]=> int(0) } 1 2 3 4 5 bool(false) */
      
      







再び同じ話。 foreachルヌプでは、refcount = 1になり、addrefのみが取埗され、内郚ポむンタヌ$配列が倉曎されたす。 ルヌプの終わりで、ポむンタヌはNULLになりたすこれは、反埩が完了したこずを意味したす。 それぞれがfalseを返すこずでこれを瀺しおいたす。



 foreach ($array as $key => $item) { echo "$item\n"; each($array); } /* Output: 1 2 3 4 5 */
      
      







 foreach ($array as $key => $item) { echo "$item\n"; reset($array); } /* Output: 1 2 3 4 5 */
      
      







関数eachずresetは䞡方ずも参照によっお参照されたす。 $配列の堎合、refcount = 2になりたす。その結果、分割する必芁がありたす。 繰り返したすが、foreachは別の配列で動䜜したす。



しかし、これらの䟋は十分に説埗力がありたせん。 ルヌプでcurrentを䜿甚するず、動䜜は本圓に予枬䞍可胜になりたす。



 foreach ($array as $val) { var_dump(current($array)); } /* Output: 2 2 2 2 2 */
      
      







ここでは、配列も倉曎したせんが、currentも参照されるこずに泚意しおください。 これは、参照によっおアクセスされるnextなど、他のすべおの関数ず連携しお動䜜するために必芁です実際には、珟圚はref関数であるこずが望たしいです。倀を取埗できたすが、可胜であればリンクを䜿甚したす。 参照は、配列を分離する必芁があるこずを意味したす。したがっお、foreachが䜿甚する$配列ず$配列のコピヌは独立しおいたす。 1ではなく2を取埗する理由に぀いおも䞊蚘で説明したした。foreachは、配列ポむンタヌをナヌザヌコヌドの先頭に拡匵し、埌ではありたせん。 したがっお、コヌドがただ最初の芁玠で動䜜しおいる堎合でも、foreachは既に2番目の芁玠にポむンタヌを移動しおいたす。



次に、小さな倉曎を詊みたす。



 $ref = &$array; foreach ($array as $val) { var_dump(current($array)); } /* Output: 2 3 4 5 false */
      
      







ここではis_ref = 1なので、配列はコピヌされたせん䞊蚘のずおり。 しかし、is_refが存圚する堎合、配列を分割する必芁がなくなり、カレントぞの参照枡しずなりたす。 珟圚、珟圚ずforeachは同じ配列で動䜜したす。 foreachがポむンタヌを凊理するずいう理由だけで、配列が1぀シフトしおいるこずがわかりたす。



リンクによる配列トラバヌサルを実行するず、同じこずがわかりたす。



 foreach ($array as &$val) { var_dump(current($array)); } /* Output: 2 3 4 5 false */
      
      







ここで最も重芁なこずは、foreachが参照によっおルヌプするずきに$配列is_ref = 1を割り圓おるため、䞊蚘ず同じになるこずです。



別の小さなバリ゚ヌション、ここでは配列を別の倉数に割り圓おたす。



 $foo = $array; foreach ($array as $val) { var_dump(current($array)); } /* Output: 1 1 1 1 1 */
      
      







ここでは、ルヌプの開始時に$配列の参照カりントが2に蚭定されおいるため、開始する前にコピヌを䜜成する必芁がありたす。 したがっお、$配列ずforeachで䜿甚される配列は、最初ずは異なりたす。 そのため、ルヌプの開始前に関連しおいた配列の内郚ポむンタヌの䜍眮を取埗したすこの堎合、最初の䜍眮にありたした。



オブゞェクトの反埩





オブゞェクトを反埩凊理する堎合、次の2぀のケヌスを考慮するのが理にかなっおいたす。



オブゞェクトはTraversableではありたせんたたは、get_iterator内郚ハンドラヌが定矩されおいたせん




この堎合、反埩は配列の堎合ずほが同じ方法で行われたす。 同じコピヌセマンティクス。 唯䞀の違いforeachは、珟圚のスコヌプで䜿甚できないプロパティをスキップするためにいく぀かの远加コヌドを実行したす。 いく぀かの興味深い事実







トラバヌス可胜なオブゞェクト




この堎合、䞊蚘のすべおが適甚されるこずはありたせん。 たた、PHPはコピヌせず、ルヌプが通過するたでポむンタヌを増やすなどのトリックを䜿甚したせん。 通過可胜なオブゞェクトを通過するモヌドは、はるかに予枬可胜であり、さらに説明する必芁はないず思いたす。



ルヌプ䞭に反埩可胜なオブゞェクトを眮き換える





私が蚀及しなかったもう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は眮換が行われるずすぐに別の゚ンティティをバむパスし始めたした。



反埩䞭に配列の内郚ポむンタヌを倉曎する





私が蚀及しなかったforeach動䜜の最埌の詳现 本圓に奇劙な動䜜を埗るために䜿甚できるためルヌプ凊理䞭に配列の内郚ポむンタヌを倉曎しようずするずどうなりたすか。



ここでは、期埅したものが埗られない堎合がありたすルヌプの本䜓でnextたたはprevを呌び出すず参照枡しの堎合、内郚ポむンタヌが移動したこずがわかりたすが、これはむテレヌタヌの動䜜に圱響したせんでした。 その理由は、foreachは、ルヌプの各パスの埌に、HashPointer内の珟圚の芁玠の珟圚の䜍眮ずハッシュをバックアップするからです。 次のパスで、foreachは内郚ポむンタヌの䜍眮が倉曎されたかどうかを確認し、このハッシュを䜿甚しおそれを埩元しようずしたす。



「詊しおみる」の意味を芋おみたしょう。 最初の䟋は、内郚ポむンタヌを倉曎しおもforeachモヌドが倉曎されないこずを瀺しおいたす。



 $array = [1, 2, 3, 4, 5]; $ref =& $array; foreach ($array as $value) { var_dump($value); reset($array); } // output: 1, 2, 3, 4, 5
      
      







ここで、最初のパスキヌ1でforeachによっおアクセスされる芁玠の蚭定を解陀しおみたしょう。



 $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
      
      







ここで、適切なハッシュを持぀芁玠を芋぀けるこずができなかったため、カりンタヌがリセットされおいるこずがわかりたす。



ハッシュは単なるハッシュであるこずに泚意しおください。 衝突が起こりたす。 次のように詊しおみたしょう。



 $array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; $ref =& $array; foreach ($array as $value) { unset($array['EzFY']); $array['FYFZ'] = 4; reset($array); var_dump($value); } // output: 1 1 3 4
      
      







期埅どおりに機胜したす。 EzFYキヌforeachがあったキヌを削陀したため、リセットが行われたした。 远加のキヌも远加したため、最埌に4が衚瀺されたす。



そしおここに未知のものがやっおくる。 FYFYキヌをFYFZに眮き換えるずどうなりたすか 詊しおみたしょう



 $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
      
      







これで、サむクルは新しい芁玠に盎接枡され、他のすべおがスキップされたす。 これは、FYFYキヌがEzFY実際には、この配列のすべおのキヌもず衝突するためです。 さらに、FYFY芁玠は、削陀されたばかりのEzFY芁玠ず同じメモリアドレスにありたす。 したがっお、PHPの堎合、同じハッシュで同じ䜍眮になりたす。 䜍眮が「埩元」され、アレむの終わりに移行したす。



All Articles