純規の暇人趣味ブログ

首を突っ込んで足を洗う

switchするよりMap使った方が綺麗な気がする

      HimaJyun

プログラムを組む時にswitchでぶんぶん分岐する事は割とアルインコだと思います。

とはいえswitchはインデントが深くなりがち。読みづらく、書きづらいので、Mapを使ったらどうか、と言う話です。

例はJavaで書いてますけど、たぶん他の言語でも部分的に通用するはず。

あと、あくまで「気がする」話、つまり私の主張でしかなく、どこにでも居るオタクが思考を駄々洩れにした戯言なので、これを見て自分の手法として取り入れるかは各々の判断にお任せするよ。

性能より書きやすさ/読みやすさ重視の話なので、性能の必要な所は他のもっと適した手法を考えてね。

switchが辛い

例えば条件が1個や2個の時はif-else-ifで良いと思いますけど、条件が増えると一致するまで愚直に評価していくif-else-ifでは効率が良いとは言えません。(逆に1個や2個の場合ifの方が早いとか言う話を聞いた事もある、真偽は知らぬ)

その点switchは多くの条件が存在する場合に適しています。(switchは二分探索が使われるらしい)

判定アルゴリズムは言語によって変わるんでしょうけれども、基本的には上記の通りです。

話がややこしくなるのでコンパイラによる最適化などは考慮しない、賢いコンパイラであればどちらであっても同様の機械語で動くはず。

書きづらい

性能が問題ではないんですよ。1000個ある条件を分岐するならともかく、普通、ifやswitchを使って書くのは精々両手で数えられる範囲ですよね。

ここでswitchの構文を確認してみましょう。

switch (条件) {
    case "foo":
        break;
    case "bar":
        break;
    case "baz":
        // breakを書き忘れてるのか次に流したいのか分からない
    default:
        break;
}

ええ、そうなんですよ、switchで1段、caseで更にもう一段インデントが深くなる。

しかもbreakが必要で、ない場合に忘れてるのか次に流したいのかが分からない。(これは言語仕様による、C#だとgotoを使わないと次に流れない)

もちろんswitchで十分なパターンもあります、例えば条件で振り分けて単純に値をreturnするだけとか。

でも、各々の条件に対して複雑な処理をしたい場合はキビシイです、例外処理でtryとかが必要になろうものなら……

Mapを使うという考え

という訳でMap(言語によっては連想配列とも呼ばれる)を使ってこんな感じに実装してみてはどうだろう?

private final static Map<String, Runnable> executor = Collections.unmodifiableMap(
    new HashMap<String ,Runnable>() {{ // Java9だとここでも型推論使えるよ
        put("foo", () -> foo()); // ラムダで処理する
        put("bar", Sample::bar); // 関数を分けたい時はメソッド参照とか
        put("baz", Sample::baz);
    }}
);

public static void execute(String args) {
    executor.getOrDefault(args.toLowerCase(Locale.ENGLISH), () -> {
        // switchのdefault、ifのelseに該当するコード
    }).run(); 
}

しかも条件を追加する時はputを増やすだけでよい。

処理の部分は単にgetしてrunしているだけで、一度書いてしまうと不変で行ける。

default(else)はgetOrDefaultを使用して値が存在しない場合の処理として表現。

toLowerCaseを使う事で大文字小文字を区別しなくても良いように出来る、Locale.ENGLISHしているのは言語設定で結果が変わる可能性を排除するため。(参考: Yukiの枝折: Android:toLowerCase/toUpperCaseに注意)

この辺りは言語仕様で左右されるけど、Javaの場合は

  1. (例ではunmodifiableMapにしているが)動的に変更する事も可能。
  2. Runnableの部分はFunctionInterfaceなら何でも良い、ConsumerとかFunctionとか(Java7でもinterfaceを用意すれば似たようなことは出来る、多少冗長になるけど)
  3. 条件の追加はラムダだけでなくメソッド参照も使えるので長いコードを関数として切り出す事も可能。
  4. キーはMapに使える物なら何でも良い、逆に言えばプリミティブ型を使いたい時はIntegerとかのラッパークラスが必要になるので少し効率が落ちる。
  5. HashMapならO(1)で分岐できる

という具合、ベースのコード量はswitchより増えるけど、条件が多い場合や処理が複雑な場合には違ってくるはず。

ちなみに、JavaのStringでswitchする場合は内部でジャンプテーブルを使って似たようなことしてるらしい。(参考: caseがStringなswitch文はmapで実装されている? - R42日記)

Enumで分岐する場合

JavaにはEnumMapというEnum特化のMapがあります。内部はEnumの配列で作られているため効率がよいです。

enum SampleEnum {
    FOO,
    BAR,
    BAZ
}

private final static Map<SampleEnum, Runnable> executor = Collections.unmodifiableMap(
    new EnumMap<SampleEnum, Runnable>(SampleEnum.class) {{
        put(SampleEnum.FOO, () -> foo());
        put(SampleEnum.BAR, Sample::bar);
        put(SampleEnum.BAZ, Sample::baz);
    }}
);

public static void execute(SampleEnum args) {
    executor.getOrDefault(args, () -> {
        // switchのdefault、ifのelseに該当するコード
    }).run();
}

Enumを条件に振り分けする場合はこちらを使いましょう。(EnumがkeyのHashMapを使ってるパターン、たまに見かけるけど無駄だよ)

本題から逸れるけど、上記の例のパターンではEnumにメソッドを実装した方がスマートだね。

Optionalでnullチェック

例えば、キーにnullを使えないMap(TreeMapとか)の場合はnullチェックが必要です。(StringでtoLowerCaseを使う場合も必要)

public static void execute(String args) {
    if(args == null) {
        // nullチェック
    }
    executor.getOrDefault(args.toLowerCase(Locale.ENGLISH), () -> {
        // switchのdefault、ifのelseに該当するコード
    }).run();
}

ここはOptionalのifPresentを使うとif不要に。

public static void execute(String args) {
    Optional.ofNullable(args).ifPresent(key -> {
        executor.getOrDefault(key.toLowerCase(Locale.ENGLISH), () -> {
            // switchのdefault、ifのelseに該当するコード
        }).run();
    });
}

まぁifでも良いとは思う、これが便利なのはメソッドの戻り値をそのままget()に流したい時(例えばQueueのpoll()でユーザーが指定した引数を左から1個ずつ処理するとか)

HashMapならnullチェック不要

HashMapはkeyにnullを使えます。

nullの時に何かをしたいならnullで処理を、nullの時に何もしたくないなら「何もしない」というラムダを入れれば良いでしょう。

private final static Map<String, Runnable> executor = Collections.unmodifiableMap(
    new HashMap<String ,Runnable>() {{
        put("foo", () -> foo());
        put("bar", Sample::bar);
        put("baz", Sample::baz);
        put(null, () -> {}); // NOP
    }}
);

public static void execute(String args) {
    // nullでも気にしない
    executor.getOrDefault(args, () -> {
        // switchのdefault、ifのelseに該当するコード
    }).run();
}

ただ、インターフェースがMapなのにHashMapの実装に依存するのもどうかと思うので、この場合型をMapからHashMapに変えるとかした方が良いかも。

もしくは普通にnullチェックをするか。

どうだろう?

こんな風にすれば比較的まともにswitchを書ける(そもそもそんな事をしなければならない時点で設計の敗北?)

「でも、私が思い付くんだし他の人がとっくに編み出してそうだよなぁ」とググってみたら似たような主張はチラホラありました。

ま……まぁ、今回は書きやすさ重視の話なので、えぇ。

……マサカリ飛んできたりしないかな、怖いなぁ

 - プログラミング