2010.01.07
yna

PHPの言語仕様は酷い

PHPのリファレンスを使ったコードを調べていたら、あまりにすごい現象があったので紹介したい。
https://www.programming-magic.com/20080307090613/

<?php
    $array = array(1,2,3); 

    $ref = &$array[1];  //参照渡し 

    $copy = $array;
    $copy[0] = 'a';
    $copy[1] = 'b';
    $copy[2] = 'c';
    foreach( $array as $v ) print "$v";     //←この出力は
?>

実はこうなる

1
b
3

ついでに、PerlとRubyで同じコードを書いて動作を比較してみた。

Perl

    @array = (1,2,3);

    $ref = \$array[1];          #   リファレンス渡し

    @copy = @array;             # 値コピー
    $copy[0] = 'a';
    $copy[1] = 'b';
    $copy[2] = 'c'; 

    print "$_\n" for ( @array );

1
2
3

Perlの場合はリファレンスと言うよりは、ポインターに近い。配列のコピーは、基本的に値でコピーされるので、動作的におかしな動作はしない。

Ruby

    array = [1,2,3]
    val = array[1]              #   基本的に参照渡し
    copy = array                #   基本的に参照渡し

    copy[0] = 'a'
    copy[1] = 'b'
    copy[2] = 'c'

    array.each{|x| print "#{x}\n";}

実行結果は、以下のようになる。

a
b
c

Rubyは全てのオブジェクトで扱われるので、実際にはcopyとarrayは同じ実体を指している。他の言語から見れば、戸惑うかもしれないが、首尾一貫している。
perlと同じように、値をコピーするには、cloneを使用して複製を作成する。

copy = array.clone			#	ここでは値を複製して渡す。

PHPのマニュアルには、こう書かれています。

<?php
    $a =& $b;
?>

この場合、$a と $b は同じ内容を指します。
注意: ここで、$a と $b は完全に 同じで、$a が $b を 指しているわけではなく、その逆でもありません。$a と $b は同じ場所を指しているのです。

Rubyを知っていた私は、基本的に変数はオブジェクトととして扱われており、普通に=だけで代入したときは、暗黙のうちに値のコピー作成して代入していると考えていた。だから、&演算子を使用すれば、このときだけ参照代入、値を複製しないで、同じ値を示すのだと思っていました。
そうならば、仕様としてはそんなにおかしくは、ありません。
さらに、マニュアルには、もう一つ注意事項が書かれています。はっきり言って、ここが異常!

注意: リファレンスを含む配列をコピーする際に、そのリファレンスが解消される ことはありません。配列を関数に値渡しする場合も同様です。

    $copy = $array
    //  $arrayが配列の場合の動作は、以下の意味だと普通は思うでしょう。

    $copy = array();
    foreach( $array as $i => $v ) $copy[$i] = $v;

    //  $arrayが配列の場合の動作は、実際にはこんな動作になっています。
    foreach( $array as $i => $v ){
        if( is_ref($array[$i])){            //  is_refはありません。
            $copy[$i] = & $array[$i];
        }
        else {
            $copy[$i] = $array[$i];
        }
    }

なんだ?

以下の例を見て欲しい。関数fooで、配列の特定の条件の要素だけをリファレンスとして保存します。そして関数barで値渡しをした(と本人は思っている)、要素を変更して返します。なんと、リファレンスを保存した値だけが、リファレンスモードになっていてもとの配列を破壊してしまいます。

<?php
    $buffer = array();
    function    foo( &$ary )
    {
        global      $buffer;
        //  特定の条件のものだけ保存
        for( $i = 0 ; $i < sizeof($ary) ; $i++ ){
            if( $ary[$i] == 2 ) $buffer []= &$ary[$i];
        }
    }

    function    bar( $ary )
    {
        for( $i = 0 ; $i < sizeof($ary) ; $i++ ){
            $ary[$i] += 10;                     //  全て書き換え
        }
        return $ary ;
    }

    $array = array(1,2,3);
    foo( $array );
    $copy = bar( $array );

    foreach( $array as $v ) print "$v\n";       //←この出力は
?>

1
12
3

こんな状態になったら処理系のバグだと思うだろう。

原因はPHPの変数と値の保持の仕方にあるようです。
基本的に変数(配列の要素も含む)は、値を保持しています。これを値モードとします。リファレンス演算子を作成した場合は変数はリファレンスモードになって、別な場所に値をコピーし、その値を差すようにポインタに変換します。また、コピー先の変数にもそのポインタを渡します。そのときリファレンスカウンターを+1します。
一方リファレンスになって変数がスコープを外れて削除された場合や、unset命令で削除された場合は、値の持っているリファレンスカウンターを減らします。リファレンスカウンターが1になったとき、最後の変数に値モードになって変数が直接値を保持するようになります。

image006

こんな現象が起きる仕様は、あまりにもお粗末というしかありません。言語デザインのセンスは酷いというか、、なんというか、

結論。はっきり言ってPHPの言語仕様は腐っている(-_-“)

writen by yna
一覧に戻る