風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

PHPのリファレンス(参照)について、自分なりにかみくだいてみる

経緯

最近、PDO で PDOStatement::bindParam を使う処理ではまったため。


bindParam()は、

public bool PDOStatement::bindParam ( mixed $parameter , mixed &$variable [, int $data_type = PDO::PARAM_STR [, int $length [, mixed $driver_options ]]] )

PHP: PDOStatement::bindParam - Manual

第二引数($variable)が、SQL ステートメントパラメータにバインドする変数名になるので、当然ながらリファレンス(参照)渡しになっている。


それで、はまったときのコードは、

<?php
// (略) ※この部分にデータベースハンドル($dbh)取得処理等

$stmt = $dbh->prepare("SELECT * FROM table_sample WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);

$id_list = array(1,2,3); // 行が存在する id のリスト
foreach ($id_list as &$id) { // ←【問題個所】 &$id を $id に書き換えることで、正常動作するようになる
    $stmt->execute();
    $row = $stmt->fetch(PDO::FETCH_ASSOC); // bool(false) となり、取得できない
    // 以下略
}

のようなもの。


変数 $id にバインドし、foreach でこれを次々と変更しながら実行する、という意図だったのだが、foreach で参照渡しにしていると、動作しない。


参照渡しを止めれば動作するようにはなるのだが、「どうしてなのか?」をきちんと説明できなかったので、そもそもPHPのリファレンスはどういう仕様なのか、というところから調べてみた次第。

さて、PHP におけるリファレンスとは?

まずは、マニュアルを参照。

PHP において、リファレンスとは同じ変数の内容を異なった名前で コールすることを意味します。これは C のポインタとは異なります。 リファレンスを使ってポインタの演算をすることはできませんし、 リファレンスは実メモリのアドレスでもありません。詳細は リファレンスが行わないこと を参照ください。 そうではなく、リファレンスはシンボルテーブルのエイリアスです。 PHP では、変数名と変数の内容は異なっており、 このため、同じ内容は異なった複数の名前を有する事が可能であることに 注意してください。最も良く似ているのは、Unix のファイル名とファイルの 関係です。この場合、変数名はディレクトリエントリ、変数の内容は ファイル自体に対応します。リファレンスは、Unix ファイルシステムの ハードリンクのようなものであると考えられます。

PHP: リファレンスとは? - Manual

むぅ、わかるようなわからないような…やっぱり、いまひとつピンとこない。
なまじ、C言語のポインタを知っているから、混乱しているのだろうか。

具体的に違和感を覚えていたポイント

以下のような PHP プログラムについて考えてみた。

<?php
$no = 1;
function    prn(&$a, &$b, &$c) {
    global $no;
    echo("({$no}) \$a={$a} \$b={$b} \$c={$c}\n");
    $no++;
}
          prn($a, $b, $c);  //  (1) $a= $b= $c=
$b = &$a; prn($a, $b, $c);  //  (2) $a= $b= $c=
$a = 'A'; prn($a, $b, $c);  //  (3) $a=A $b=A $c=
$b = 'B'; prn($a, $b, $c);  //  (4) $a=B $b=B $c=
$c = 'C'; prn($a, $b, $c);  //  (5) $a=B $b=B $c=C
$a = &$c; prn($a, $b, $c);  //  (6) $a=C $b=B $c=C
$a = "X"; prn($a, $b, $c);  //  (7) $a=X $b=B $c=X
$b = "Y"; prn($a, $b, $c);  //  (8) $a=X $b=Y $c=X
$c = "Z"; prn($a, $b, $c);  //  (9) $a=Z $b=Y $c=Z
$a = $b ; prn($a, $b, $c);  //  (10) $a=Y $b=Y $c=Y
$b = "O"; prn($a, $b, $c);  //  (11) $a=Y $b=O $c=Y
$c = $b ; prn($a, $b, $c);  //  (12) $a=O $b=O $c=O

違和感を覚えたのは、

  • (2) で、「$b = &$a」としてリファレンス代入を行い、その後は $a に値を代入すると $b にも反映されるようになっている(3)。
  • ところが、(6) で「$a = &$c」として、$a に対してリファレンス代入を行うと、$b に対しては直接何もしていないにも関わらず、以降は $a を変更しても、$b には反映されなくなってしまう(7)。

というところ。
直感的に「$a にリファレンス代入等の操作を加えたとしても、$b は影響されずに $a を指し示したままであり、$a の内容を書き換えると、そのまま $b にも反映される」ものとばかり思っていた。

自分なりのリファレンス代入の解釈

マニュアルを読み返すなどして、ようやく理解できた(かも)。

上述のプログラムの、動作概要を図に示すと、

http://f.st-hatena.com/images/fotolife/f/furyu-tei/20140924/20140924114654_original.png
のようになると思われる。
ただし、上記プログラム内では表示(echo)をしている関係上、まだ割り当てていない変数も参照され、このときにNULLが割り当てられてしまうので、図とは厳密には異なってくる。


ポイントとしては、

  • (2) で、$a が初めて参照されているが、変数が初めて参照される際には、新規に値"NULL"(もしくは、通常の代入の場合は右辺の値)が入った内容が確保され、変数(図では$a)は当該内容を示すシンボル(同s1)を保持する。
  • (2) の「$b = &$a」というリファレンス代入では、変数 $a の内容を示すシンボル(同s1)を、変数 $b にコピーしている。
    結果として、$a と $b は、同じ内容を指し示すシンボル(s1)*1を持った、完全に等価な変数となる。
  • (3) の「$a = "A"」では、$a が持つシンボル(s1)が示す内容の値を書き換える(NULL→"A")。すると、同じs1を保持する $b も同じ内容を持つことになる。
    また、(4) の「$b = "B"」では、逆に $b の値を書き換えることで、同じs1を保持する $a も同じ内容になる。
    すなわち、この時点での $a と $b とは、実際にまったくの等価であることを示している。
  • (6) の「$a = &$c」というリファレンス代入では、変数 $c の内容を示すシンボル(同s2)を、変数 $a にコピーしている。
    結果として、$c と $a は、同じ内容を指し示すシンボル(s1)を持った、完全に等価な変数となる。
    代わりに、$a(シンボル:s2) と $b(シンボル:s1) とでは、指し示す内容が異なることになり、違う値を示すようになる。

C言語畑の人向けに解説

C言語に慣れている人用に、PHP のリファレンス代入を、疑似的にC言語で表現してみた。

/*
 * PHPの変数代入とリファレンス代入の動作をC言語に置き換えるサンプル
 */

/*
<?php
function prn(&$a, &$b, &$c) {echo("\$a={$a} \$b={$b} \$c={$c}\n");}

$b = &$a;
$a = "A";
$b = "B";
$c = "C"; prn($a, $b, $c);
$a = &$c; prn($a, $b, $c);
$a = "X"; prn($a, $b, $c);
$b = "Y"; prn($a, $b, $c);
$c = "Z"; prn($a, $b, $c);
$a = $b ; prn($a, $b, $c);
$b = "O"; prn($a, $b, $c);
$c = $b ; prn($a, $b, $c);
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 1024

void    prn(char * a, char * b, char * c) {printf("$a=%s $b=%s $c=%s\n", a, b, c);}

int     main(void) {
    char * a, * b, * c;
    char * s1, * s2;

    /* $b = &$a; */ s1 = calloc(BUFFER_SIZE, 1); a = s1; b = a;
    /* $a = "A"; */ strcpy(a, "A"); // $a = "A";
    /* $b = "B"; */ strcpy(b, "B"); // $b = "B";
    /* $c = "C"; */ s2 = calloc(BUFFER_SIZE, 1); c = s2; strcpy(c, "C"); prn(a, b, c);
    /* $a = &$c; */ a = c;                                               prn(a, b, c);
    /* $a = "X"; */ strcpy(a, "X");                                      prn(a, b, c);
    /* $b = "Y"; */ strcpy(b, "Y");                                      prn(a, b, c);
    /* $c = "Z"; */ strcpy(c, "Z");                                      prn(a, b, c);
    /* $a = $b ; */ strcpy(a, b);                                        prn(a, b, c);
    /* $b = "O"; */ strcpy(b, "O");                                      prn(a, b, c);
    /* $c = $b;  */ strcpy(c, b);                                        prn(a, b, c);

    return 0;
}

これを実行すると、

$a=B $b=B $c=C
$a=C $b=B $c=C
$a=X $b=B $c=X
$a=X $b=Y $c=X
$a=Z $b=Y $c=Z
$a=Y $b=Y $c=Y
$a=Y $b=O $c=Y
$a=O $b=O $c=O

こんな感じで、PHP と同様の結果になる。


ポイントとしては、PHP における変数 $a・$b に該当するものを、C言語におけるポインタ変数(char *) a・b とみなしたとき、

  • PHP の値代入($a = "A")は、C言語では strcpy(a, "A") に相当。
    ※このとき、ポインタ変数 a の値は変わらない。
  • PHP のリファレンス代入($b = &$a)は、C言語では b = a に相当。
    ※このとき、ポインタ変数 b の値が、ポインタ a のものに置き換わる(結果として、指し示す先が直前とは異なってくる)。

ということ。


C言語では、

b = &a;

のように記述する場合、b はポインタ(例えば int *型)であり、変数 a は実体(例えば int型)で、'&a' は a のアドレスを指す。つまり、変数 a と b とでは、そもそもの型が異なっている。


一方で、PHP では、

$b = &$a;

のように記述すると、これはリファレンス代入であり、(変数が内部的にもっている)内容を指し示すシンボルのコピーという意味あいであり、この結果、変数 $a と $b とは、本質的に等価となる。


なので、('&' という記号だけをみて)上記を混同するとはまってしまう。
どちらかといえば、PHP におけるリファレンス代入は、C言語における「(変数等の)アドレスの、ポインタ変数への代入」よりも、「ポインタ変数からポインタ変数への代入」にイメージとしては近いと考えられる。

最初の問題については?

最初に書いた、bindParam() がらみの不具合だが、foreach ループの 1 回目で、

$id = &$id_list[0];

と等価になるが、これでは $id のシンボルが $id_list[0] のシンボルと同じものへと置換されてしまうため、その前の bindParam() 実行時点で指定した $id の保持していたシンボル(=PDOStatementオブジェクト($stmt)内部で保持している変数のシンボル)と異なってしまい、結果として、SQL ステートメントパラメータ(':id') には、bindParam()実行時点での $id の値(=NULL) が使われてしまうので、意図した動作にならなかった、と考えられる。



リファレンスの罠(追記:2014/09/24)

「配列内部のリファレンスは危険もある」、と。

しかし、配列の内部のリファレンスは危険もあるということに気をつけましょう。 通常の (リファレンスではない) 代入の右辺にリファレンスを使っても 左辺はリファレンスには変わりませんが、配列の内部のリファレンスは通常の代入のままとなります。 これは、関数をコールする際に配列をリファレンスで渡すときも同じです。

PHP: リファレンスが行うことは何ですか? - Manual

こ、これはわかりにくい上に、はまりそうだ…。

リファレンスを使用しない場合

単純に、配列(array)$1 を $2 に代入する場合。

<?php
$a1 = array(1);
$a2 = $a1;
$a2[0] ++; // $a1 == array(1), $a2 == array(2)
$a2[0] ++; // $a1 == array(1), $a2 == array(3)

*1:なお、"シンボル"という呼び名は、この記事中での便宜上のものである。適切な呼称はなんだろう?