161
76

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 3 years have passed since last update.

【PHP8.0】厳密なswitch文ことmatch式が使えるようになる

Last updated at Posted at 2020-06-22

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

他言語でのmatch構文

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式を使っていくとよいでしょう。

161
76
7

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
161
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?