読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

switch文の罠

PHPで(知らずに)以下のようなコードを書いていてはまった。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行すると以下のような結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「$val == 3」のときはスキップされるから、「sum=12」になるはず‥

ためしに色んな言語で(時には無理矢理(謎))同じことをしてみる。

Javaの場合

public class Hoge {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0; i < 5; ++i) {
            int val = i + 1;

            System.out.printf("%3d%n", val);
            switch (val) {
            case 1:
                System.out.printf("%5d: %s%n", val, "One");
                break;
            case 2:
                System.out.printf("%5d: %s%n", val, "Two");
                break;
            case 3:
                System.out.printf("%5d: %s%n", val, "Three");
                continue;
            case 4:
                System.out.printf("%5d: %s%n", val, "Four");
            case 5:
                System.out.printf("%5d: %s%n", val, "Five");
                break;
            }
            sum += (val);
        }
        System.out.printf("sum=%d%n", sum);
    }
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」になる。

C言語の場合

#include <stdio.h>

int main(int argc, char* argv[]) {
    int i;
    int sum = 0;
    int val;

    for (i = 0; i < 5; ++i) {
        val = i + 1;

        printf("%3d\n", val);
        switch (val) {
        case 1:
            printf("%5d: %s\n", val, "One");
            break;
        case 2:
            printf("%5d: %s\n", val, "Two");
            break;
        case 3:
            printf("%5d: %s\n", val, "Three");
            continue;
        case 4:
            printf("%5d: %s\n", val, "Four");
        case 5:
            printf("%5d: %s\n", val, "Five");
            break;
        }
        sum += val;
    }
    printf("sum=%d\n", sum);
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

bash

#! /bin/bash

sum=0
for ((i = 0; i < 5; ++i)); do
    val=$((i + 1))

    printf "%3d\n" $val
    case $val in
        1)
            printf "%5d: %s\n" $val "One"
            ;;
        2)
            printf "%5d: %s\n" $val "Two"
            ;;
        3)
            printf "%5d: %s\n" $val "Three"
            continue;
            ;;
        4|5)
            case $val in
                4)
                    printf "%5d: %s\n" $val "Four"
                    ;;
            esac
            printf "%5d: %s\n" $val "Five"
            ;;
    esac
    sum=$((sum + val))
done
printf "sum=%d\n" $sum

break文に当たる「;;」を省略できないので、fall throughの部分は無理矢理だが。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」

Perl

Perlは書けないので、以下の辺りを参考に。

use Switch;

my $i;
my $sum = 0;

for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next;
        }
        case [4, 5] {
            switch ($val) {
                case 4 {
                    printf("%5d: %s\n", $val, "Four");
                }
            }
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「sum=15」だ‥
下記で追調査。

Ruby

Rubyも書けないので、以下の辺りを参考に。

ちなみに、脱線するが、この辺りが面白かった。

sum=0
for i in 1..5 do
    val = i;

    printf("%3d\n", val);
    case val
    when 1
        printf("%5d: %s\n", val, "One");
    when 2
        printf("%5d: %s\n", val, "Two");
    when 3
        printf("%5d: %s\n", val, "Three");
        next;
    when 4, 5
        case val
        when 4
            printf("%5d: %s\n", val, "Four");
        end
        printf("%5d: %s\n", val, "Five");
    end
    sum += val;
end
printf("sum=%d\n", sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

一旦まとめると‥

言語 switch(case)文の中でのループスキップ
PHP  ×(※1)
Java  ○
C言語  ○
bash  ○
Perl  ×(※2)
Ruby  ○

と、こんな感じになった。
‥って書いちゃうと誤解を招くので、さっさと追調査。

(※1)PHPの場合の仕様

実は、マニュアルの中にこんなことが書いてあった。


注意: 他の言語とは違って、 continue命令は switchにも適用され、breakと同じ動作をします。 ループの内部でswitchを使用しており、 外側のループの処理を続行させたい場合には、continue 2 を使用してください。

‥おぉ!
ということで、以下のように書き直してみる。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue 2;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

この構文、もちろん単なる多重ループのときにも有効で、変なラベル(何)をつける必要がないというメリットがある。一方で、ぱっと見ではどこに飛ぶのかが分からず、ループの数を数え間違えるとひどい事になるというデメリットがある。

(※2)Perlの場合の仕様

(PHPと同じように書いてみたときのエラーメッセージからたどり着いたのは内緒(謎))

Perlでは、ラベルによって、どのループに対する「next」なのかを指定できる。

そもそも、PerlのSwitchモジュールにおける「next」はfall throughのためのものらしい。

つまり、先のPerlプログラム中の「case 3」よりも下に、$valが3のときにマッチするcase句があったら違う結果を引き起こしていたというわけだ。怖い怖い‥

というわけで、上記を踏まえて書き直したのが以下。

use Switch;

my $i;
my $sum = 0;

loop:
for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next loop;
        }
        case 4 {
            printf("%5d: %s\n", $val, "Four");
            next;
        }
        case [4, 5] {
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

Switchモジュールのfall throughの仕様をちゃんと使ってみた。
また、この「ラベル」の概念はJavaでも同じですね。(もっと言えば、悪しきモノとして封印されているC言語のgotoとか(略))
で、実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

で‥

自分の場合、JavaC言語bashスクリプトに親しんでからPHPを使うようになった派なのでこのような罠にどっぷりはまったわけだが、「構文が似ているからと言って、同じ動作をするとは限らない」という教訓になる。
上層の人(誰)の「君はこのプログラム言語できるよね?こっちの言語も似たようなものだから余裕でしょ?ちょっとこっちの案件手伝ってくれる?あ、言語の習得は案件を進めながら合間にやってね」という台詞に、全力で立ち向かえることと思う。「似ているからこそ危険なんだよ!」と。