LoginSignup
17
19

More than 5 years have passed since last update.

PHPのお勉強2

Posted at

概念・言語仕様・補完的な実装(の続き)

参考:

PHPのオブジェクト指向基礎
http://www.objective-php.net/basic/interface

PHP: 言語リファレンス - Manual
http://goo.gl/TFWYSf

リファレンス

この項は、PHPの癖のある仕様があるようで意外と難しい。

・リファレンスは実メモリのアドレスではない、ポインタの演算も出来ない。
・リファレンスはシンボルテーブルのエイリアス。
・リファレンスの代入は同じもの(シンボルテーブル)を参照するようになる。
・オブジェクトのインスタンス化ではPHP5以降ではリファレンスが返されます。
・PHP 5 以降では、リソース型のようなオブジェクト変数は 実際のデータへの単なるポインタとなりました。
・関数の内部で global 宣言された変数にリファレンスを 代入すると、そのリファレンスは関数の内部でのみ参照可能となります。 これを避けるには、$GLOBALS 配列を使用します。

test.php
function global_references($use_globals)
{
    global $var1, $var2;
    if (!$use_globals) {
        $var2 =& $var1; // 関数の内部でのみ参照可能
    } else {
        $GLOBALS["var2"] =& $var1; // 関数の外部でも参照可能
    }
}

この結果がまだよく分からない。どういうこと?
配列要素のリファレンスの代入部分では、右辺もリファレンスなっているみたい。
なにそれ。

test.php
<?php
/* スカラー変数への代入 */
$a = 1;
$b =& $a;
$c = $b;
$c = 7; // $c はリファレンスではないので $a や $b の値は変わりません

/* 配列変数への代入 */
$arr = array(1);
$a =& $arr[0]; // $a および $arr[0] は同じリファレンスセットになります
$arr2 = $arr; // これは、リファレンス代入ではありません!
$arr2[0]++;
/* $a == 2, $arr == array(2) */
/* $arr の内容は、たとえリファレンスでなくても変わります! */
?>

自分で試してみた。順序としてはこういうことらしいけど。。なぜ。。

test.php
Interactive shell

php > $arr = array(1);
php > var_dump($arr);
array(1) {
  [0]=>
  int(1)
}
php > $a=&$arr[0];
php > var_dump($arr);
array(1) {
  [0]=>
  &int(1)
}
php > unset($a);
php > var_dump($arr);
array(1) {
  [0]=>
  int(1)
}

どうやらcopy-on-write(またはリファレンスカウンティングreferences-counting)*1 方式のなせる技らしいことは分かってきた。
このサイトはシンボルテーブルの図示による説明が丁寧。

なぜ通常代入で七面倒なことをするのか-references counting
通常の代入であれば、最初っから値をコピーすればいいじゃん、というのは、未来にはそうなるかもしれないが、メモリという大切なリソースを効率的に使うための方策としてこのような形式となっている。通常代入時($b=$a時)に値をコピーしてしまうと、同じ内容のものをメモリに持っておくことになる。それは非効率だから、今後、違うことになるときまでは一緒にいましょうね、ということだ。さらに、違う値になることがないのであれば、値のコピーそのものがいらないということもあり得よう。
このような方式を copy-on-write(またはリファレンスカウンティングreferences-counting)*1 方式という。

apd_get_active_symbolsによってシンボルテーブルをのぞくことが出来る模様。

xdebugでもシンボルテーブルをのぞくことができるみたい。
そして、refcountが配列の挙動の根本かなあ。。

この例では、新しいシンボル名(a)が現在のスコープで作成され、 新しい変数コンテナが string 型と値 new string で作成されます。 「is_ref」ビットはデフォルトで FALSE にセットされます。なぜなら、ユーザーランド 参照が作成されないからです。 この変数コンテナを利用するシンボルが1つだけあるので、「refcount」は 1 に設定されます。 「refcount」が 1 ならば、is_ref が常に FALSE である点に 注意してください。 もし » Xdebug をインストール済みなら、 xdebug_debug_zval()を呼ぶと、この情報を表示できます。

このあたりをみていて、参照カウント法というより、配列をコピーしたときには参照ごとコピーしてしまうことが直感と実際の処理の認識のズレになっていることがわかった。

test.php
<?php
$a = array(1, 2, 3); // 配列を準備
$c = &$a[1];         // 配列の要素への参照
$b = $a;             // 配列をコピー
$b[0] = 9;           // コピーした配列に変更を加える
unset($c);           // 「配列の要素への参照」をunset()
$b[1] = 0;
// $aの値は?
var_dump($a);

少々むずかしい。

事例として計測の仕方を参考。ベンチマーク結果も参考。

test.php
// 参照なし
function nop_noref($arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡しのみ
function nop_ref_arg(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡し+参照返し
function &nop_ref_both(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

別スコープとの参照のやりとりは返しも参照にするほうがいいってことかな。関数だけかな。

おい参照渡し、参照しない場合に対して処理時間が3700倍とかどないなっとんじゃい。
...
参照返しをしなければ、戻り値の受け取りのとき、配列のコピーが生まれているのです。

下記は、実際にはポインタではなくて、シンボルテーブルがうまく動作して間に立ってくれているからなのかな。

そう、PHPの変数は、最初からすべてポインタなのです。だから特別な記号を使わなくても、いくらでも変数を関数に引き渡せるのです。いやそうとしか説明できないでしょ、この結果見たら。
...
zval の is_ref がどうとかあたりのちゃんとした説明は、2013-03-07 - bravewood の日記 で読めます。中身にこだわる方はどうぞ。
...
まあ、そもそも話で、LLな言語の変数がミュータブルなのはしょうがないですが、であるからこそ、別のスコープではできるだけイミュータブルな値であるように意識して扱うのが、うまいプログラムのお作法ですよね。
状態の変化は、それを意図したメソッドを持つオブジェクトでのみ起こるべきです。
...

ここで「ポインタ」についてググる

文脈上ではメモリアドレスを表す意味が強そうだな。

ポインタ (pointer)とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照するものである。有名な例としてはC/C++でのメモリアドレスを表すポインタが挙げられる。
なお、C++では、さらに独立した「参照」というものがある(#参照の節を参照)。

ここで「イミュータブル」とか「破壊的」についてググる

どこで値が変わるか把握できない、混乱するようなコードにはするなってこととか。(大雑把な理解)
状態の管理も構造的に行ってなんとなくでやるなとか。
イミュータブルにオブジェクトを保つのは非破壊的なメソッドか。

http://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%9F%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB
http://qiita.com/chocolamint/items/020143cf249505bf2ffc

オブジェクト指向プログラミングにおいて、イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル(mutable)なオブジェクトで、作成後も状態を変えることができる。

記事に戻って読み進める。

PHPにおいて参照が役に立つ場面としては2点が挙げられている。
独自に実装する場合にはクロージャーが使いこなせるようになるならありかもな。
引数の方は使わなくてもいいかもな。どうだろう。便利さの意図がくみ取れてないかも。

preg_match()の第三引数は参照渡しです。このコールで、$matchは宣言されている必要がありません。このように、コール元のスコープの変数の代入式と同じように働く参照渡しは、時として役に立ちます。
...
$removed の参照を束縛しています。ここがもし参照でなかったら、まさに引数に配列を渡したときのように、クローンに対して操作され、$removed=array(); が維持されてしまいます。

なるほど、そうなのね。PHP以外のLL言語を考えるとスタンダードでもない仕様ということかな。

Rubyはデフォルトが参照。今まで触った言語はすべてこの挙動だった。

要点とかルートが提示されている説明で足がかりに。

sort()やshuffle()やparse_str()等、参照渡しの動きをするものもある
...
慣れるまではマニュアルをチェックするようにし、その関数に渡される配列が「$arr」で定義されているか「&$arr」で定義されているか確認するようにしましょう。
...
シンボルテーブルについて
http://php.net/manual/ja/features.gc.refcounting-basics.php
...
PHPの配列は、典型的な「配列」ではなく、「ハッシュテーブル」です。チェイン法で実装されています。
http://php.net/manual/ja/language.types.array.php

ブロックスコープがないので参照を一度unsetしないとおかしくなる場合。怖い。
デバッグの様子を1行ずつ丁寧に説明しているのでわかりやすかった。

1周目:$array[0]の値を$valueの参照する先へ代入。つまり$array[2]に$array[0]を代入($array[0]=1, $array[2]=1)。
2周目:$array[1]の値を$valueの参照する先へ代入。つまり$array[2]に$array[1]を代入($array[1]=2, $array[2]=2)。
3周目:$array[2]の値を$valueの参照する先へ代入。つまり$array[2]に$array[2]を代入($array[2]=2, $array[2]=2)。

test.php
<?php
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
// $arr は array(2, 4, 6, 8) となります
unset($value); // 最後の要素への参照を解除します

なるほど、参照だけですむのなら、下手に関数内で引数を処理するなと。特に大きい配列では。
これは普段から意識した方が良いレベルかもな。

関数に配列が引数として与えられた場合、PHPは
その時点ではスタック領域のポインタアドレスを渡しているが、
配列に変更があった時点で
その配列のヒープ領域がコピーされ別の変数として
新しいスタック領域に別のポインタアドレスが保存される
...
関数に与えられた配列型の引数は変化を加えると初めてコピーされるという事は、
配列の中身を変える必要の無い処理は
配列から中身を取り出し、配列の外でテキスト処理、演算処理を加えてやる方が
配列をコピーする処理が省かれるので
処理スピードが早いと推測される。
配列の要素が多いと差が顕著になると思われる。

渡されたリファレンスをさらにリファレンスにしても元々の変数のリファレンスを変えることは出来ない。っということかな。。

test.php
php > $bar=1;
php > $baz=3;
php > function foo(&$var)
php > {
php {     $var = 4;
php {     $var =& $GLOBALS["baz"];
php {     echo $var;
php { }
php > foo($bar);
3
php > echo $bar;
4

参照周りを理解しておかないで使うとこういうことになるっぽい。

以下の条件を「全て満たした」時に、処理速度が「ビックリするぐらい」遅くなります。
ただ、まだ結論が完全には出ていない(というか出せない)ので、
以下の条件以外でも起きるかもしれませんけど。
あと、以下の文中内の「関数」は「メソッド」に置き換えても通じます。
1.繰り返し文(for、foreach、whileのいずれか)の中で関数呼び出しをしている
2.関数への引数の渡し方が値渡しである
3.関数の中で値渡しされた引数の値が変更されている
4.3の引数または、3の引数と参照関係にある(イコール代入をしている)変数を戻り値で返していて且つ、関数の呼び元で戻り値を任意の変数に代入している
5.3の引数のデータ型が文字列か配列である

関連で気になる箇所

PHP: 参照カウント法の原理 - Manual
http://goo.gl/fgFAU5

extract: 配列からシンボルテーブルに変数をインポートする (配列 関数) - PHPプロ!マニュアル
http://goo.gl/qdcxhG

変数管理の基礎(PHPの参照とは何か)-PHP変数管理 (2) - CPA-LABテクニカル
http://goo.gl/ij4k6J

シンボルテーブルの中身を確認してみたい - PHP - 教えて!goo
http://goo.gl/zA5Fi5

同一変数名による参照の参照 - モノノフ日記
http://goo.gl/MKI5jY

日記/2007/06/19/PHPのzvalの扱い、シンボルテーブルの扱いにはほとほと愛想が尽きた。 - Glamenv-Septzen.net
http://goo.gl/eDTaZP

ガベージコレクション

・参照の循環などで片付けられなかったり余分にメモリを使ってしまうメモリリークが場合によっては問題となる。
・オブジェクトでは暗黙で参照によって使われるので起きやすいかもとのこと。
・動きとしてはrefcountを数えたり増減させてメモリリークを片付ける。
・速度が少々犠牲(最大7%目安)になるがガーベッジコレクションしながら処理を続けることができるので有用。
・詳細なデバッグにはコンパイル時に環境変数を指定するとよい。

この構造体を指すシンボルがいかなるスコープにももはや存在しないにもかかわらず、 配列要素「1」がこの同じ配列をまだ指すので、片づけられません。 それを指している外部シンボルがないので、ユーザーがこの構造体を片付ける方法が ありません。このようにしてメモリーリークとなります。 幸いにも、PHP はリクエスト終了後、このデータ構造を片付けます、しかし、それ以前に これはメモリ内の貴重な空間を占めています。 この状態は、「親」要素に再帰する「子」を持つ構文解析アルゴリズムなどの実装中に しばしば発生します。 もちろんオブジェクトでも同じ状態が起こり得ます。オブジェクトは常に暗黙のうちに 参照によって使われるので、実はその状態がより起こりそうです。

17
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19