これは 豊田高専コンピュータ部 Advent Calendar 2019 | 3 日目の記事です.
はじめに
つい先日の 11 月 28 日,ついに PHP 7.4 がリリースされました.
プロパティの型ヒンティングが追加されるなど,目玉機能が多いアップデートになりました.
その中の一つに, 匿名関数の新書式 (アロー関数) があります.
PHP 7.3 以前では,匿名関数を以下のように書いていました:
function ($args) use ($globals) {
return doSomething($args, $globals);
}
PHP の匿名関数 (Closure) は,記述されたスコープを維持しないので, 外は別の世界 となります.
したがって,外のスコープの変数を使用するときは use
を用いる必要がありました.
PHP 7.4 からは,以下のように書くことができます:
fn($args) => doSomething($args, $globals)
この 2 つは等価といえますが,後者では use
を省略できます.
実は,ここに大きな罠があったのです.
(PHP 7.4 で新しくできた罠ではありませんが,より意識しづらくなってしまいました.)
なにが起きるのか
以下のようなコードを考えてみましょう.
$i = 0;
call_user_func(
fn() => echo ++$i . PHP_EOL,
);
echo $i . PHP_EOL;
匿名関数の中で $i
をインクリメントしたあと出力し,その後グローバルスコープで $i
の値を出力するコードです.
一見すると,どちらでも 1
が出力されるように思えてしまいます.
このコードを実行したときの出力は以下のようになります:
1
0
匿名関数内でインクリメントした $i
の 値が維持されていない ことがわかります.
どこが罠なのか
先ほどのコードを PHP 7.3 以前の書式に書き直してみましょう:
$i = 0;
call_user_func(
function () use ($i) {
echo ++$i . PHP_EOL;
},
);
echo $i . PHP_EOL;
この書式にすると use
で明示的に外のスコープの $i
が使われていると分かります.
しかし,この use
における宣言は単なる宣言ではないのです.
匿名関数に入るとき,外のスコープの変数は,仮引数と同じように 値がコピーされます .
PHP ではオブジェクト (クラスのインスタンス) や配列も既定で値渡しされます.
したがって,匿名関数内でインクリメントしたのはコピーに過ぎないので, 抜けた後には反映されません .
どうすればいいのか
旧書式では,明示的に参照渡しをすることで解決できます:
- function () use ($i) {
+ function () use (&$i) {
出力は以下のようになるはずです:
1
1
しかし,この方法は 新書式には使えません .
なぜなら, use
を省略するようになったことで参照渡しの &
を明示的に宣言する場所がないからです.
( fn () use (&$i) => ...
とすることも今のところできません)
なにが悪いのか
旧書式では use
で宣言することで 仮引数の一つのような振る舞い をすることが一目瞭然でした.
しかし,新書式では明示的な宣言がないため, 他の言語 で行えるように外のスコープの変数も読み書きできるように思えてしまいます.
use
がないことでその違いに気づけず,罠にはまってしまうのです.
まとめ
近年,セミコロンをはじめ,不要なものはできる限り省略することが増えているのではないかと,個人的に思います.
省略はコードを簡潔に保つうえで大切ですが,可読性を犠牲にしたことでこういった罠が増えることのないようにしていきたいですね. (これは余談ですが,セミコロンは付ける派です.)
釣りのようなタイトルで長々と書いてしまいましたが,この罠にかかって小一時間費やすといった,私のような人が少しでも減ればいいなと思います.