ユーザの住んでいる国を取得します。
しかし、うっかりユーザがnullでした。
どうなるでしょう。
$user = null;
echo $user->address->country ?? '';
PHPのプロパティは元よりnull安全なので、存在しないプロパティだろうがnullだろうがプリミティブ型だろうがいきなりプロパティを取り出せます。
取れない場合はE_NOTICEが発生しますが、??
を使えば黙殺できます。
しかしメソッドはだめです。
echo $user->getAddress()->getCountry() ?? ''; // Fatal error: Call to a member function getAddress()
律儀にエラーを潰すとこんなかんじになります。
// 1
if (method_exists($user, 'getAddress')) {
if (method_exists($user->getAddress(), 'getCountry')) {
echo $user->getAddress()->getCountry();
}
}
// 2
if ($user instanceof \User) {
if ($user->getAddress() instanceof \Address) {
echo $user->getAddress()->getCountry();
}
}
まあ面倒ですね。
ということでPHPにも流行りのnullが導入されることになりました。
以下は該当のRFC、Nullsafe operatorの日本語訳です。
Nullsafe operator
Introduction
このRFCでは、Nullセーフ演算子?->
を提案します。
Proposal
メソッドを呼び出したり、計算結果からプロパティを取り出したりといった作業は、nullではない対象にだけ行いたいのが普通です。
現在のPHPではnullチェックを入れる必要があるので、入れ子や繰り返しが多くなります。
$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}
// $countryが取り出せた
nullセーフ演算子?->
を使うと、このコードは以下のように書けます。
$country = $session?->user?->getAddress()?->country;
演算子の左側がnullだった場合はチェーン全体の実行が停止され、nullが返ります。
nullでなかった場合は通常の->
と全く同じ動作になります。
Short circuiting
Introduction
ショートサーキット、短絡とは、条件に基づいて式の実行をスキップすることです。
よくある例としては&&
と||
などです。
nullセーフ演算子について、短絡を実装する方法は3種類ありました。
どの方法がよいか、以下の同じコードスニペットで見ていきましょう。
null?->foo(bar())->baz();
1. Short circuiting for neither method arguments nor chained method calls
・引数もメソッドも全て実行する、短絡しない。
このような例は、今のところHackくらいでしか見られません。
関数bar()
もメソッドbaz()
も両方とも実行されるため、baz()
でCall to a member function on null
のエラーが発生します。
3つの選択肢の中で、最も予想外の結果となるでしょう。
前回のRFCでの大きな問題点でした。
2. Short circuiting for method arguments but not chained method calls
・引数は飛ばすけどメソッドは飛ばさない。
これは、通常は短絡しないと呼ばれるものです。
関数bar()
は呼び出されませんが、メソッドbaz()
は呼び出されてCall to a member function on null
のエラーが発生します。
3. Short circuiting for both method arguments and chained method calls
・引数もメソッドも全てスルー
これは完全短絡と呼ぶことにしましょう。
関数bar()
もメソッドbaz()
も呼び出されず、エラーは発生しません。
Proposal
このRFCでは、完全短絡を実装します。
チェーン内のひとつの要素の評価に失敗した場合、それ以降の全てが中断され、チェーン全体がnullと評価されます。
以下の要素はチェーンの一部とみなされます。
・配列[]
・プロパティ->
・null安全プロパティ?->
・静的プロパティ::
・メソッドコール->
・null安全メソッドコール?->
・静的メソッドコール::
以下はチェーンに含まれず、別のチェーンが発生します。
・関数の引数
・配列[]
内部の式
・{}
にプロパティアクセスした場合->{}
チェーンは最も短くなるように自動判別されます。
以下はその例です。
$foo = $a?->b();
// --------------- chain 1
// -------- chain 2
// $aがnullだったらchain2が中断、b()は実行されず$foo=nullになる
$a?->b($c->d());
// --------------- chain 1
// ------- chain 2
// $aがnullだったらchain1が中断、b()は実行されず$foo=nullになる。chain2も実行されない。
$a->b($c?->d());
// --------------- chain 1
// -------- chain 2
// $cがnullだったらchain2が中断、d()は実行されない。chain1の$a->b()は実行されて引数はnullになる。
Rationale
この動作を選択した理由。
1. It avoids surprises
$foo = null;
$foo?->bar(expensive_function());
驚きを最小にします。
$foo
がnullであればexpensive_function()
の実行は望ましくないでしょう。
2. You can see which methods/properties return null
$foo = null;
$foo?->bar()->baz();
短絡がないと、チェーン内のメソッド呼び出しやプロパティアクセス全てにnullセーフ演算子を使用しなければならなくなります。
不要な部分には使わないことで、どのメソッドやプロパティに問題があるのかを特定することもできます。
3. Mixing with other operators
$foo = null;
$baz = $foo?->bar()['baz'];
var_dump($baz);
// 短絡がない場合:Notice: Trying to access array offset on value of type null
// このRFC:null
短絡によって配列アクセス['baz']
は完全にスルーされるので、E_NOTICEは発生しません。
Other languages
Stack Overflow 2020 surveyによる人気の高水準言語、および姉妹言語Hackについて、nullセーフ演算子の実装状況を確認してみます。
言語 | 有無 | 表記 | 短絡 |
---|---|---|---|
JavaScript | 〇 | ?. | 〇 |
Python | |||
Java | |||
C# | 〇 | ?. | 〇 |
TypeScript | 〇 | ?. | 〇 |
Kotlin | 〇 | ?. | × |
Ruby | 〇 | &. | × |
Swift | 〇 | ?. | 〇 |
Rust | |||
Objective-C | ※1 | ||
Dart | 〇 | ?. | × |
Scala | ※2 | ||
Hack | 〇 | ?→ | ※3 |
※1:nilへのプロパティやメソッド呼び出しは常に無視される
※2:DSL経由で可能
※3:メソッド引数も評価する
13のうち8言語がnullセーフ演算子を持っており、そのうち4言語は短絡評価します。
Syntax choice
?
が、短絡が発生する正確な場所を表します。
これは、nullセーフ演算子を実装している他の全ての言語と似ています。
Forbidden usages
Nullsafe operator in write context
nullセーフ演算子を代入に使用することはできません。
$foo?->bar->baz = 'baz';
// Can't use nullsafe operator in write context
foreach ([1, 2, 3] as $foo?->bar->baz) {}
// Can't use nullsafe operator in write context
unset($foo?->bar->baz);
// Can't use nullsafe operator in write context
[$foo?->bar->baz] = 'baz';
// Assignments can only happen to writable values
このRFCの以前のバージョンでは、=
の左側にnullセーフ演算子を使った場合、nullであれば代入をスキップすることが提案されていました。
しかし、技術的な問題からこの仕様は外されました。
今後のRFCで追加されるかもしれません。
References
リファレンスは許可されません。
参照するためには変数やプロパティのメモリ上の値が必要となりますが、nullセーフ演算子はnullを返すことがあるからです。
$x = &$foo?->bar;
// おおむね↓と同じ
if ($foo !== null) {
$x = &$foo->bar;
} else {
$x = &null;
// Only variables should be assigned by referenceのエラー
}
従って、以下のような例は禁止となります。
$x = &$foo?->bar;
// Compiler error: Cannot take reference of a nullsafe chain
takes_ref($foo?->bar);
// Error: Cannot pass parameter 1 by reference
function &return_by_ref($foo) {
return $foo?->bar;
// Compiler error: Cannot take reference of a nullsafe chain
}
引数に参照を渡せるかどうかはコンパイル時にはわからないため、2番目の例は実行時エラーになります。
Backward Incompatible Changes
既知の後方互換性のない変更はありません。
Future Scope
PHP7.4以降、nullに配列アクセスnull["foo"]
するとE_NOTICEが発生します。
そのため、演算子?[]
も許可し、$foo?["foo"]
と書けるようになると有用かもしれません。
しかし三項演算子$foo?["foo"]:["bar"]
と曖昧になってしまうため、このRFCには配列へのnullセーフ演算子は含まれていません。
投票
期間は2020/07/31まで、投票者の2/3の賛成で受理されます。
2020/07/20時点では賛成44反対2で、まず確実に導入されます。
感想
正直null安全があまり理解できてないので、このRFCがどのくらい有用なのかもよくわかっていません。
個人的には、メソッドチェーンが書きやすくなるくらいしかメリットを感じられていないです。
そもそもPHPでは元々型がアバウトなうえ、うっかり下手にぬるぽが起こってたとしてもメモリを壊したりなんてことはまずできませんからね。
最悪でもせいぜい500 Internal Server Errorになる程度です。
しかし投票を見るに、union型やstr_containsを遥かに超え、JITやmatch式に匹敵するほどの圧倒的賛成多数です。
それだけ有用であると皆に判断されたということで間違いないでしょう。
PHPにおけるnull安全の優秀さはきっと誰かが教えてくれるはず。