まだ参照渡しで消耗してたの? 僕はPHPerなのでPHPのことしかわからないけど、だいたいPHPで参照渡しのことを気にしても時間の無駄だし積極的に利用するべき場面が極めて限定されることはPHPのリファレンス(参照&)の傾向と対策、あるいはさよならに書いたので読んでほしい。
それとPHPマニュアルでは「参照渡し」ではなく「リファレンス渡し」と呼ぶが、どちらにせよ、わざわざ「渡し (call by-
)」を付けて呼ぶ意義は乏しいので、これからは「変数リファレンス」として覚えてほしい。もうちょっと具体的なことはPHP: リファレンスとは? - Manualを読んで。
端的に言うと、PHPでは「オブジェクト引数はデフォルトで引数渡しされる」「オブジェクトのプロパティ(メンバー変数)は参照渡し」のような理解は誤解であり、完全に嘘です。
ふしぎなふしぎなオブジェクト
PHPの配列とオブジェクトは、似たような書きかたをしてもまったく異なる挙動をします。
以下のコードはarray(連想配列)とオブジェクトで異なる挙動をします。
function setPrice1(array $a, float $tax): array
{
if (!isset($a['prime_cost'])) {
$a['prime_cost'] = $a['price'];
$a['tax'] = $tax;
$a['price'] = $a['prime_cost'] + $a['prime_cost'] * $tax;
}
return $a;
}
function setPrice2(stdClass $a, float $tax): stdClass
{
if (!isset($a->prime_cost)) {
$a->prime_cost = $a->price;
$a->tax = $tax;
$a->price = $a->prime_cost + $a->prime_cost * $tax;
}
return $a;
}
動かしてみましょう (練習問題)
<?php
$ary = ['name' => 'りんご', 'price' => 100];
echo "before:";
var_dump($ary);
$bry = setPrice1($ary, 0.03);
echo "after:";
var_dump($bry);
var_dump($ary === $bry);
echo PHP_EOL;
////////////////////
$obj = (object)['name' => 'りんご', 'price' => 100];
echo "before:";
var_dump($obj);
$obj2 = setPrice2($obj, 0.03);
echo "after:";
var_dump($obj2);
var_dump($obj === $obj2);
だいじなのは $ary === $bry
と $obj === $obj2
のところです。おなじように書いても別の結果になりましたでしょうか。
なぜこうなるかと言ったら、PHPの配列は代入(および関数呼び出しの引数)によってコピーされると定義されているからです。 (PHP: 配列 - Manual)
配列への代入においては、常に値がコピーされることに注意してください。
どうしてそうしたのかというと作者がこうなるようにPHPを設計したからと表現するしかないのですが、実際この挙動の差は便利なことの方が多いです。この話はオブジェクトをいい感じに複製(クローン)する [myclabs/deep-copy] - 超PHPerになろうにも書きました。
この例ではstdClass
を使ってますが、これを実コードで積極的に活用することはおすすめしません。クラスを定義してください。
問題は、この両者の差異を「PHPは参照渡しだから」「オブジェクトは参照渡しされるから」のような意味不明な謎解説をして初心者を騙すひとが居ることです。繰り返しますが、このオブジェクトの挙動は「変数リファレンス」とは無関係です。
PHP4のオブジェクトについて
これは昔話なので忘れてよいのですが、PHP4のオブジェクトのプロパティは現在の配列と同様にコピーされていました。
PHP4時代のオブジェクトを現在に近い使い勝手にするために常用されていたのがオブジェクトの引数に&
を付けて変数リファレンスを受けるというテクニックです。古いコードには稀にこの痕跡が残っていますが、これは過去にPHP4系との相互運用性が考慮されていた名残といえます。単にPHP5の変化を知らずに慣習として引き摺っただけかもしれないですね。
それでも疑り深いひとのための補足
ここでは「日時オブジェクトを日本時間の正午にセットする」3種類の函数を用意してみましょう。ここではPHP7を用いています。
function setdate1_オブジェクト(DateTime $dt): void
{
$dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt->setTime(12, 0);
}
function setdate2_変数リファレンス(DateTimeInterface &$dt): void
{
$dt = $dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt = $dt->setTime(12, 0);
}
function setdate3_イミュータブル(DateTimeImmutable $dt): DateTimeImmutable
{
$dt = $dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt = $dt->setTime(12, 0);
return $dt;
}
課題
この三種類の実装を比較し、それぞれの利点と欠点を挙げて考察せよ。
レポートは来週の講義開始前までに提出のこと
参考にするとよい資料
- PHP: DateTimeInterface - Manual
- PHP: DateTime - Manual
- PHP: DateTimeImmutable - Manual
- イミュータブル - Wikipedia
- Psr7を使ってみた(というか不変オブジェクトを初めて使った感想) - Qiita
ヒント
このような.php
ファイルを用意し、呼び出し側と実装コードをそれぞれ変更し出力を観察するとよい。
<?php
echo "---------------------------------", PHP_EOL;
echo "[setdate1_オブジェクト] (DateTime)", PHP_EOL;
echo "---------------------------------", PHP_EOL, PHP_EOL;
$dt1 = new DateTime;
echo "before:";
var_dump($dt1);
setdate1_オブジェクト($dt1);
echo "after:";
var_dump($dt1);
echo PHP_EOL;
////////////////////
echo "-------------------------------------", PHP_EOL;
echo "[setdate2_変数リファレンス] (DateTime)", PHP_EOL;
echo "-------------------------------------", PHP_EOL, PHP_EOL;
$dt2_1 = new DateTime;
echo "before:";
var_dump($dt2_1);
setdate2_変数リファレンス($dt2_1);
echo "after:";
var_dump($dt2_1);
echo PHP_EOL;
////////////////////
echo "----------------------------------------------", PHP_EOL;
echo "[setdate2_変数リファレンス] (DateTimeImmutable)", PHP_EOL;
echo "----------------------------------------------", PHP_EOL, PHP_EOL;
$dt2_2 = new DateTimeImmutable;
echo "before:";
var_dump($dt2_2);
setdate2_変数リファレンス($dt2_2);
echo "after:";
var_dump($dt2_2);
echo PHP_EOL;
////////////////////
echo "----------------------------------------------", PHP_EOL;
echo "[setdate3_イミュータブル] (DateTimeImmutable)", PHP_EOL;
echo "----------------------------------------------", PHP_EOL, PHP_EOL;
$dt3 = new DateTimeImmutable;
echo "before:";
var_dump($dt3);
$dt3 = setdate3_イミュータブル($dt3);
echo "after:";
var_dump($dt3);
echo PHP_EOL;
function setdate1_オブジェクト(DateTime $dt): void
{
$dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt->setTime(12, 0);
}
function setdate2_変数リファレンス(DateTimeInterface &$dt): void
{
$dt = $dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt = $dt->setTime(12, 0);
}
function setdate3_イミュータブル(DateTimeImmutable $dt): DateTimeImmutable
{
$dt = $dt->setTimezone(new DateTimeZone('Asia/Tokyo'));
$dt = $dt->setTime(12, 0);
return $dt;
}
追記1: 関数呼び出しは特に関係ないしプロパティも参照渡しでも暗黙的に変数リファレンスの代入になるようなことはない
上記の例では関数呼び出しをまたいでいますが、同じ関数内のローカル変数でも同じことです。
改めて強調します。オブジェクトと配列の以下のような振舞からPHPのプロパティもリファレンス渡しであるかのように誤解されることがありますが、そうではありません。
$ary = ['author' => '太宰治', 'title' => '人間失格'];
$obj = (object)$ary; // stdClass にキャスト
// それぞれ別オブジェクトにコピーする
$ary2 = $ary;
$obj2 = $obj;
// この時点では === で等値になることを確認
var_dump([
'array' => ['===' => $ary === $ary2, $ary, $ary2],
'object' => ['===' => $obj === $obj2, $obj, $obj2],
]);
$ary['author'] = 'Osamu Dazai';
$obj->author = 'Osamu Dazai';
// arrayは結果が分かれ、objectは一致する
var_dump([
'array' => ['===' => $ary === $ary2, $ary, $ary2],
'object' => ['===' => $obj === $obj2, $obj, $obj2],
]);
この挙動は変数代入と似てなくもないので「なるほどオブジェクトの代入は参照渡しなんだな」と誤解するのも理解はできます。
$title = '人間失格';
$title2 = &$title; // 変数のリファレンスを代入
$title = 'Ningen Shikkaku';
var_dump([
'variable' => ['===' => $title === $title2, $title, $title2],
]);
似てるように見えるかもしれませんが、プロパティの代入が暗黙的に変数リファレンス(参照渡し)になることはありません。
以下のコードでプロパティに値を代入したときと明示的に変数リファレンスを代入したときで振舞が異なることを確認できます。
// オリジナルのタイトル
$orig_title = '人間失格';
// 今度はコピーではなく別オブジェクトとして作成する
$obj1 = (object)['author' => '太宰治', 'title' => $orig_title];
$obj2 = (object)['author' => '太宰治', 'title' => $orig_title];
// 新しいタイトル
$new_title = '人間合格';
$obj1->title = $new_title;
$obj2->title = &$new_title;
// $new_title に "人間失格" を代入
$new_title = $orig_title;
var_dump([
'title' => ['===' => $obj1->title === $obj2->title, $obj1->title, $obj2->title],
]);
そもそも $obj->author = 'Osamu Dazai'
という構文が許可されている時点で =
の右辺が暗黙的に変数リファレンスになるということはありえません。別物。
まれに「配列は値型でオブジェクトは参照型」という説明がされることがありますが、そうではありません。PHP(7.0以降)の実装として内部的には「IS_REFERENCE
型」という型があります(PHP7における内部値の表現―パート1 : PHP5とPHP7のzvalの仕組み | POSTD)が、この型は配列とオブジェクトの違いを反映するためのものではなく、変数リファレンス(&
)を表現するものです。
あくまで配列はPHP: 配列 - Manualでの説明通り、配列は代入および関数呼び出しによって常にコピーされて(いることになって)います。
配列への代入においては、常に値がコピーされることに注意してください。配列をリファレンスでコピーする場合には、 リファレンス演算子を使う必要があります。
ただし、配列を別の変数に代入したり関数呼び出しをする度に新しい配列を作成して内容を全てコピーして… ということを行っていては実行効率が大変悪いので、PHP: 参照カウント法の原理 - Manualのメカニズムによって別の配列を作成する必要が生じたときだけコピーを行なっています。この最適化をコピーオンライト(CoW: Copy on Write)といいます。
このPHPの配列の振舞を指して「PHPの配列を別の変数に代入すると別の配列になるのはCoWだから」という説明がなされることもありますが、これも大嘘です。
意味としてはPHPの全ての値は&
を付けない代入や関数呼び出しの引数として渡されるごとにコピーされます。先に書いた「なんでこうなるかと言ったらこうなるようにPHPが設計されたからと表現するしかない」という表現は「PHPの配列は代入によってコピーされると定義されている」ということを指しています。
関数呼び出し時に引数の値がコピーされることを一般的に値渡し(call by value)と呼ぶのです。ただし「毎回コピーされる」というのははコードの意味としてはということであり実際にはそれほどコピーが起こらないのは、PHP処理系の実装上の最適化(CoW)によってコピーされるタイミングを遅らせる(必要なければコピーせずに消す)からです。この最適化によってユーザーはメモリ使用量の最適化など多くの恩恵を受けています。
追記2: 配列と他言語のデータ構造との比較
この配列のコピーという振舞を奇妙に感じるでしょうか。人間はダブルスタンダードを持っているので、別の箇所ではこの振舞を当然のものとして受け入れています。
<?php
$f = 'ガッ';
$g = $f;
$f = 'ぬるぽ';
echo $f, $g, PHP_EOL; // "ぬるぽガッ"
$a = [];
$a['msg'] = 'ガッ';
$b = $a;
$a['msg'] = 'ぬるぽ';
echo $a['msg'], $b['msg'], PHP_EOL; // "ぬるぽガッ"
$o = new stdClass;
$o->msg = 'ガッ';
$p = $o;
$o->msg = 'ぬるぽ';
echo $o->msg, $p->msg, PHP_EOL; // "ぬるぽぬるぽ"
代入先が変数への直接代入か、配列の固定キーへの代入か、オブジェクトの固定プロパティへの代入かによって挙動が別れています。
これを雑にRubyで書き直すとこうなります。
f = 'ガッ'
g = f
f = 'ぬるぽ'
puts "#{f}#{g}" # "ぬるぽガッ"
a = {} # Hash: PHPの配列に相当
a['msg'] = 'ガッ'
b = a
a['msg'] = 'ぬるぽ'
puts "#{a['msg']}#{b['msg']}" # "ぬるぽぬるぽ"
require 'ostruct'
o = OpenStruct.new # なんでも代入できるオブジェクト
o.msg = 'ガッ'
p = o
o.msg = 'ぬるぽ'
puts "#{o.msg}#{p.msg}" # "ぬるぽぬるぽ"
Rubyの変数とPHPの変数の振舞、その後のRubyのOpenStruct
とPHPのstdClass
の振舞はそれぞれ、およそ同じと見て差し支えないでしょう。RubyのHash
とPHPの配列の振舞が自然と感じられるかは個人の感性や経験によるでしょうが、PHPの配列は変数の延長線上にある非オブジェクトのデータ構造として提供されていると捉えることもできます。
PHPであってもRubyのHash
の挙動に近似させたければArrayObject
を使うかクラス定義するかという選択肢が用意されています。
<?php
$a = new ArrayObject;
$a['msg'] = 'ガッ';
$b = $a;
$a['msg'] = 'ぬるぽ';
echo $a['msg'], $b['msg'], PHP_EOL; // "ぬるぽぬるぽ"
PHPの配列の特性をよく知って安全にコードを書きましょう。