2009年01月11日

PHP:循環参照に対する動作の違い

・ 循環参照を持つ変数に何らかの処理を加えた時の動作の相違
・ 循環参照を見つける簡易関数(php5)

自前環境で、循環参照に対する幾つかの関数の動作を試行。
ちなみに、php v5.3 では循環参照をガーベッジ・コレクターが検出し、循環参照に使用されているメモリーを解放することができるとのこと。

■ 循環参照の例
A)配列で値渡し(※)
$a = array();
$a[0] = $a;

B)配列で参照渡し
$a = array();
$a[0] = &$a;

C)クラスインスタンスで値渡し
$a = new stdClass;
$a->p = $a;

D)クラスインスタンスで参照渡し
$a = new stdClass;
$a->p = &$a;


※ うーむ。これは循環参照というのかなぁ???

■ 自前の環境
PHP Version 5.2.3
System Windows NT
Server API Apache 2.0



 動作の相違

上記A)〜D)に以下のスクリプトを実行してみた・・・。
・ A),B)の場合
var_dump ($a);
echo "<br>";
echo serialize($a) . "<br>";
var_dump (unserialize(serialize($a)));

・ C),D)の場合
var_dump ($a);
echo "<br>";
echo serialize($a) . "<br>";
var_dump (unserialize(serialize($a)));
echo "<br>";
var_dump (clone $a);
echo "<br><br>";


A)配列で値渡し
array(1) { [0]=> array(1) { [0]=> *RECURSION* } }
a:1:{i:0;a:1:{i:0;a:1:{i:0;N;}}}
array(1) { [0]=> array(1) { [0]=> array(1) { [0]=> NULL } } }

B)配列で参照渡し
array(1) { [0]=> &array(1) { [0]=> &array(1) { [0]=> *RECURSION* } } }
a:1:{i:0;a:1:{i:0;R:2;}}
array(1) { [0]=> &array(1) { [0]=> &array(1) { [0]=> *RECURSION* } } }

C)クラスインスタンスで値渡し
object(stdClass)#1 (1) { ["p"]=> object(stdClass)#1 (1) { ["p"]=> *RECURSION* } }
O:8:"stdClass":1:{s:1:"p";r:1;}
object(stdClass)#2 (1) { ["p"]=> object(stdClass)#2 (1) { ["p"]=> *RECURSION* } }
object(stdClass)#3 (1) { ["p"]=> object(stdClass)#1 (1) { ["p"]=> object(stdClass)#1 (1) { ["p"]=> *RECURSION* } } }

D)クラスインスタンスで参照渡し
object(stdClass)#3 (1) { ["p"]=> &object(stdClass)#3 (1) { ["p"]=> *RECURSION* } }
O:8:"stdClass":1:{s:1:"p";R:1;}
object(stdClass)#4 (1) { ["p"]=> object(stdClass)#4 (1) { ["p"]=> *RECURSION* } }
object(stdClass)#5 (1) { ["p"]=> &object(stdClass)#3 (1) { ["p"]=> &object(stdClass)#3 (1) { ["p"]=> *RECURSION* } } }

なんて結果。

A)のシリアライズで異なったのは驚いた。serialize のマニュアルにも「配列/オブジェクト内の 循環参照も保存されます。その他の参照は失われます。 」なんて書いてある。
んが、Aは「参照渡し」ではないんだよなぁ。なので、値自体を与える必要があるためにいつかは打ち切らなきゃならんからこのようになるのか・・・。まあ、そもそも循環参照やAのようなものをセッションに入れるなんて事はしないけど、一応留意点かな。

clone もまた面白い。clone することで、# 後の数値がカウントアップされるのは、仕様上納得。ただ RECURSION と認識されるまでにもう1ステップ掛っているところは、当然の帰結なのに、これも一瞬戸惑った。まー。「なるほど」という具合ですか。

ちょっと良く分からないのはC)の4番目 clone 後に再度生成したD)
$a = new stdClass;
に同一の # の数値が振られているところ。これは一体なぜなんだろう・・・。

そして、思わずニヤリと笑ってしまうのはDの3番目。Dの1番目ではちゃんと & がついているのに、3番目では消えている。

■ ちなみにコイツらだと・・・
var_export($a);
$a == $a[0];


上記いずれも例外で処理が止まる。
Fatal error: Nesting level too deep - recursive dependency? in …
但し php4 では var_dump でもフリーズする。
実は、clone も物によってフリーズするし、A,Bの場合、in_array など(結局は == と同等か。)では Fatal error を起こす。

var_dump はまだフリーズなどの経験はないけど、循環参照のメモリリークなど別の要素も兼ね合って var_dump を切っ掛けに簡単にフリーズするのでは?なんて想像。

 でも折角なので判定関数を作成

循環参照があるかどうか判定する簡単な関数

// 配列・オブジェクト内に循環参照があれば true
function detect_circular_reference($obj){
  ob_start();
  var_dump($obj);
  $out = ob_get_clean();
  $txs = explode(' *RECURSION*', $out);
  array_pop($txs);
  foreach($txs as $txt){
    $str = explode('string', $txt);
    // 前方に string が無ければ true
    if(($cnt = count($str) - 1) === 0){
      return true;
    }
    $str = $str[$cnt];
    preg_match("/^\((\d+)\)\s\"/", $str, $match);
    if(count($match) == 2){
      // 前方の string に入らなければ true
      if($match[1] + strlen($match[0]) < (strlen(bin2hex($str)) / 2)){
        return true;
      }
    }else{
      // データ不良
      trigger_error('abnormal data!');
      return false;
    }
  }
  return false;
}

まー。var_dump が吐く文字列を処理してるだけのしょうもないもの。
実際使うかどうかも不明(笑)

一応、string に隠れている *RECURSION* は、無視するようにしてるけど、*RECURSION* なんて文字列があり得なければ、この文字列の有無の検査だけでも判定できる。

var_dump を信用してだけど・・・。

むちゃくちゃ久し振りの更新
それでも極々たまーにコメント頂いたりするので、やっぱ辿り着く人もいるんだなぁと。
ずいぶん前に独立してからは、あまりに忙しくてこういう趣味の「無駄な?お勉強」ができなくて残念。
まー。seesaa さんの機嫌を損ねて消されない程度にでも更新しよっと。

posted by HiFa at 18:25 | 🌁 | Comment(0) | TrackBack(0) | ときどきPHP | このブログの読者になる | 更新情報をチェックする
>>> スパムコメントは消してますよん。 お互い無駄な労力は避けましょう。 <<<

この記事へのコメント

コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。