RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【PHP8】match式/switch文の違いとバグの入りにくさ

f:id:tech-rakus:20200917144339p:plain

はじめに

PHP8で導入されるmatch式が導入されます。プログラマとしてはどういった場面で使いやすいのか、バグが入りやすさはどうなのかといった点が気になるのではないかと思います。 この記事では、match式についてswitch文との違いを述べながら、構文の性質からどういった場面で役立ちそうかを私なりに考えまとめました。

match式とは

match式の基本的な構文と、switchとの相違点を大まかに記載します。 簡単に言ってしまうとswitchのような分岐の構文です。

$param = 1;

$result = match($param) {
  1 => "1の結果",
  2 => "2の結果",
}

// 与えられた引数($param)に対して一致するケース(1または2)の結果(=> の右側)を返す
// 結果:$result = "1の結果"

下記にswitch文との大まかな違いを記載します。

switch match
返り値 なし あり
breakの記述 必要 不要
各条件の処理 ブロックで記述可能 1行でしか書けない
比較 緩やか(==) 厳密(===)
どの条件にも当てはまらない場合 そのまま実行される エラーになる

次の章以降では、PHP8で導入されるmatch式がどのような場面で生きてきそうかを紹介します。

match式の詳しい仕様は下記もご参照して頂くとより分かりやすいかと思います。
参考:
wiki.php.net qiita.com

matchは式。値を代入する場面で真価を発揮する

比較表で返り値が、matchは「あり」、switchは「なし」と記載しました。 これは、matchがであり、ifやswitchがであるためです。 この章では、式と文の違いを見た上で、matchが役立ちそうな場面を紹介します。

文とは

ざっくり説明すると、返り値を返さず、右辺に配置できないものです。
例:ifやswitchがこれにあたります。右辺に配置できないため直接値の代入はできません。

// 下記のような直接値を代入するような書き方は「文」ではできない

$result = if($param == 1){…

$result = switch($param){…

このため、各条件での代入が必要であり、代入忘れの恐れがあります。

// ifやswitch文では各条件で代入が必要。ケースが増えると代入忘れが怖い

/** if **/
if($param == 1){
    $result= "1の結果";
} elseif($param == 2){
    $result = "2の結果";
}

/** switch **/
switch ($param) {
    case 1:
        $result = "1の結果";
        break;
    default:
        $result = "defaultの結果";
}

式とは

ざっくり説明すると、返り値を持ち「=」の右辺に配置できるもの。また文としても使えます
例:matchがこれにあたります。直接値の代入が可能なので、代入を各条件に記述する手間がなく、代入忘れもなくなります。

$result = match($param) {
    1 => "1の結果",
    2 => "2の結果",
};

もちろん不要であれば代入しない書き方もできます。

match($param) {  // $result = の部分は取り外し可能
    1 => func_hoge(),
    2 => func_huga(),
};

参考: jsprimer.net

breakが不要。バグが入りにくくシンプルな記述ができる

switchでは明示的にbreakの記述が必要です。breakを毎回書く必要があり書き忘れのチェックが必要なので、労力がかかります。ただし、そのまま次の条件の処理を実行(フォールスルー)する記述は簡単に書けます。

switch

breakは明示的に記述が必要。フォールスルーがデフォルトであり、フォールスルーを想定した記述は簡単にできますが、その代わりにbreak忘れによるバグが入りやすい構造になっています。

switch ($param) {
    case1:
        func_hoge();// break忘れ
    case2:
        func_fuga();// case 1でヒットしても処理fugaも実行される
}

ただし、case1の場合はcase1,2を、case2のときはcase2のみを実行したい場合は上記のコードであっています。
breakを書かないことによりフォールスルーを簡単に実現できますが、その分「break忘れ」というバグが入り込む副作用を持っています

match

フォールスルーできない。その代わりbreakを明示的に記載する必要がなく、break忘れを気にしなくてよいです。

 match ($param) {
    1 => func_hoge(),
    2 => func_fuga(),
};

フォールスルー(case1の場合はcase1,2を、case2のときはcase2のみを実行)したい場合はmatchには向きません。 というのも、基本的にフォールスルーしない仕組みであることに加え、matchは条件の右辺に1行しか記載ができないからです。 このため、上記のswitchと同じことをしようとすると下記のようになります。

 match ($param) {
    1 => func_hogefuga(),  // ここに2行以上の記述はできない
    2 => func_fuga(),
};

function func_hogefuga() {// 関数切り出しが必要になる
    func_hoge();
    func_fuga();
}

条件の記入忘れにエラーで気付ける

分岐に限らず、予期せぬ値が代入される場合 や 意図せず何も代入されなかった場合は、エラーやログで気付ける方がよいです。 match式ではどの条件にもあてはまらない場合エラーになるため、誤りに気づきやすいです。

switch

条件が抜けていてもそのまま実行されます。
switchのようなエラーがでない仕組みの場合、バグが発生した時にケース忘れの可能性を考えることが必要となります。数行ならよいですが、ケースや処理が多い時は確認が大変です。

foreach ($users as $user) {
  switch ($user->score) {
  case "good":
    $command = "upgrade";
    break;
  case "bad":
    $command = "BAN";
    break;
    // $user->scoreがnormalの場合、どのケースも通らず値の代入が行われない。
    // このため、次のループ処理にそのまま$commandの値が引き継がれる
  }
  
  $commandQue[$user->id] = $command;
}
// batchキューに入れる
// 日時で実行されるバッチで普通のユーザがBANされてしまう

match

条件が抜けているとエラーになります。
matchのようにエラーとなる仕組みの場合、ケースの記述忘れを気にする必要がりません。

foreach ($users as $user) {
  $command = match ($user->score) {
    "good" => "upgrade",
    "bad" => "BAN",  // normalの場合はケースが無いので、エラーとなり、処理が中断される
   }

  $commandQue[$user->id] = $command;
}

厳密な比較

matchは厳密な比較のため「数値だと思っていたら文字列であり思わぬ分岐に入った」といったことががなくなります。 ただし、PHPはそもそも厳密な比較を前提として作られているとは言えないので、厳密な比較にすることでエラーになることもあるので注意が必要です。(おまけで記載します)

switch

$hoge = "hoge";

switch ($hoge) {
  case 1:
    echo "1の結果";
    break;
  case "hoge":
    echo "hogeの結果";
    break;
}

// "1の結果"

match

$hoge = "hoge";

match ($hoge) {
  1 => "1の結果",
  "hoge" => "hogeの結果",
}

// "hogeの結果"

一見すると厳密な比較の方が良さそうです。ただし、元々型を気にせず気軽に書けることが特徴でもあったPHP
古いコードでmatchを導入する際にはまずは、型をしっかり整備するなど対策が必要そうです。

おまけ

PHPの関数が厳密な型を意識した作りでないため、思わぬエラーが発生する例を紹介します。

例:拡張子でケースを分けたい場合

下記の例では画像ファイル名の拡張子から処理を分岐させるコードです。

$img_name = "img.jpg";

/** switch_正規表現 **/
switch (true) {
    case preg_match('/(\.jpg)$/', $img_name):
        echo "jpgだよ";
        break;
    case preg_match('/(\.png)$/', $img_name):
        echo "pngだよ";
        break;
   // gif等その他の分岐が続く
    default:
        echo "defaultだよ";
}

// => "jpgだよ"

/** match_正規表現 **/
echo match (true) {
    preg_match('/(\.jpg)$/', $img_name) => "jpgだよ",
    preg_match('/(\.png)$/', $img_name) => "pngだよ",
    // gif等その他の分岐が続く
    default => "defaultだよ"
};

// => "defaultだよ"

実はpreg_matchは該当する場合trueでなく、1を返します。このためmatch式ではtrue===1となり、正規表現に該当してもそのケースに入ることはありません。
このように既存の関数の性質を正しく理解していないと思わぬバグを生む可能性があります。

おわりに

この記事ではPHP8で導入されるmatch式について、switchと比較しながら、有効そうな場面やバグの入りにくさについて記載しました。 今後導入を検討している方やmatch式について知りたい方の一助になっていれば幸いです。

Copyright © RAKUS Co., Ltd. All rights reserved.