241
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【PHP8.0】PHPにヌルセーフ演算子が導入される

Last updated at Posted at 2020-07-20

ユーザの住んでいる国を取得します。
しかし、うっかりユーザが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を遥かに超え、JITmatch式に匹敵するほどの圧倒的賛成多数です。
それだけ有用であると皆に判断されたということで間違いないでしょう。
PHPにおけるnull安全の優秀さはきっと教えてくれるはず。

241
150
4

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
241
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?