PHPがよく言われる問題点のひとつとして、switchが曖昧な比較であるということが挙げられます。
switch($x){
case 1:
'$xは1だよ';
break;
case "1":
'$xは"1"だよ';
break;
}
case "1"
に到達することは決してありません。
ということで厳密な比較を用いるswitchことmatch構文のRFCが提出されました。
以下はMatch expression v2の日本語訳です。
PHP RFC: Match expression v2
Proposal
このRFCは、switchに似ていますが、より安全なセマンティクスを持つmatch構文の提案です。
例として、Doctrineのクエリパーサを挙げます。
// Before
switch ($this->lexer->lookahead['type']) {
case Lexer::T_SELECT:
$statement = $this->SelectStatement();
break;
case Lexer::T_UPDATE:
$statement = $this->UpdateStatement();
break;
case Lexer::T_DELETE:
$statement = $this->DeleteStatement();
break;
default:
$this->syntaxError('SELECT, UPDATE or DELETE');
break;
}
// After
$statement = match ($this->lexer->lookahead['type']) {
Lexer::T_SELECT => $this->SelectStatement(),
Lexer::T_UPDATE => $this->UpdateStatement(),
Lexer::T_DELETE => $this->DeleteStatement(),
default => $this->syntaxError('SELECT, UPDATE or DELETE'),
};
Differences to switch
switch構文と異なる点です。
Return value
後で使いたい値をswitch内で生成することは非常によくあることです。
switch (1) {
case 0:
$result = 'Foo';
break;
case 1:
$result = 'Bar';
break;
case 2:
$result = 'Baz';
break;
}
echo $result; //> Bar
そして$result
に代入し忘れることもよくあるミスです。
さらに深くネストされていた場合は、$result
がしっかり代入されているか確認するのもたいへんです。
それに対し、match
は実行した結果が評価される式です。
これによって多くの定型文を削除することができ、代入忘れというミスがなくなります。
echo match (1) {
0 => 'Foo',
1 => 'Bar',
2 => 'Baz',
};
//> Bar
No type coercion
switch文は緩やかな比較==
を使います。
これは直感に反する結果をもたらすことがあります。
switch ('foo') {
case 0:
$result = "Oh no!\n";
break;
case 'foo':
$result = "This is what I expected\n";
break;
}
echo $result;
//> Oh no!
match式は厳密な比較===
で比較します。
strict_types
の設定に関わらず常に厳密です。
echo match ('foo') {
0 => "Oh no!\n",
'foo' => "This is what I expected\n",
};
//> This is what I expected
No fallthrough
switchフォールスルーは、多くの言語でバグの温床となっています。
各case
は明示的にbreak
しないかぎり、次のcase
へと実行が継続されます。
switch ($pressedKey) {
case Key::RETURN_:
save();
// break忘れた
case Key::DELETE:
delete();
break;
}
match式では、暗黙のbreak
を付与することで、この問題を解決します。
match ($pressedKey) {
Key::RETURN_ => save(),
Key::DELETE => delete(),
};
複数条件で同じコードを実行したい場合は、条件をカンマで区切ります。
echo match ($x) {
1, 2 => 'Same for 1 and 2',
3, 4 => 'Same for 3 and 4',
};
Exhaustiveness
switchでよくあるもうひとつの問題は、全てのcaseに対応していない場合の処理です。
switch ($operator) {
case BinaryOperator::ADD:
$result = $lhs + $rhs;
break;
}
// BinaryOperator::SUBTRACTを渡しても何も起こらない
これが原因で、よくわからないところでクラッシュしたり、想定していない動作をしたり、なお悪いときにはセキュリティホールの原因になったりします。
$result = match ($operator) {
BinaryOperator::ADD => $lhs + $rhs,
};
// BinaryOperator::SUBTRACTを渡すと例外が発生する
match式はどのcaseにも当てはまらなかった場合はUnhandledMatchErrorを発するので、間違いに早期に気付くことができます。
Miscellaneous
Arbitrary expressions
matchする条件を任意の式にすることができます。
比較条件はswitch同様上から順に判定され、マッチした以後の条件は評価されません。
$result = match ($x) {
foo() => ...,
$this->bar() => ..., // foo()がマッチしたらここは呼ばれない
$this->baz => ...,
// etc.
};
Future scope
この項目は将来の予定であり、このRFCには含まれません。
Blocks
このRFCでは、match式の本文はひとつの式でなければなりません。
ブロックを許すかについては、別のRFCで議論します。
Pattern matching
パターンマッチングについても検討しましたが、このRFCには含めないことにしました。
パターンマッチングは非常に複雑であり、多くの調査が必要です。
パターンマッチングについては別のRFCで議論します。
Allow dropping (true)
$result = match { ... };
// ↓と同じ
$result = match (true) { ... };
Backward Incompatible Changes
match
がキーワードreserved_non_modifiers
として追加されます。
以下のコンテキストで使用することができなくなります。
・名前空間
・クラス名
・関数名
・グローバル定数
メソッド名およびクラス定数としては引き続き使用可能です。
Syntax comparison
Vote
投票は2020/07/03まで、投票者の2/3の賛成で受理されます。
2020/06/22時点では賛成20反対1となっていて、よほどの問題でも発生しないかぎり受理されるでしょう。
感想
switchでよく問題になっていた曖昧な比較やbreakし忘れといったミスが、構文レベルで不可能となります。
そのため、match式に従っておけばswitchに起因する問題はほぼ発生しなくなるでしょう。
またmatch全体が返り値を持ってるのも便利ですね。
そのかわり、case内部には1式しか書けないため、複数の変数値を変更したり入れ子にしたりといった複雑な処理を書くことは難しくなります。
また、あえてbreakを書かずに継続したい場合も面倒な書き方になります。
// 1ならfooとbarを、2ならbarだけ実行したい
switch($x){
case 1:
foo();
case 2:
bar();
break;
}
// after
match($x){
1 => foo() && bar(),
2 => bar(),
};
{}
で括って複数の文を書けるようにするかどうかは、アロー関数同様今後の課題となっています。
従って本RFCは、決してあらゆるswitchを置き換える構文ではなく、アロー関数のように一部のswitch文を置き換えることができる短縮構文という立ち位置になります。
しかし、よほど変なことでもしていないかぎり、大抵のswitch文はmatch式に置き換えることができると思います。
安全性のためにも、今後はできるだけmatch式を使っていくとよいでしょう。