kazasiki's blog

プログラミングとかVRゲームとか

この分岐をどうやって書く?

1.今回の問題

複雑な分岐の書き方で迷ったことはありますか?
今回皆さんに試しに書いていただきたい分岐の処理はこちら。簡単だけどちょっと面倒くさいものです。
f:id:kazasiki:20130615100507j:plain
2,3分で書いたので少し汚いけど、許して(^_^;)

文章で説明すると、変数flagの値によって処理を分岐させます。
flagの値が1の場合は、judge()関数を動かし、結果がtrueであれば、doSomething()を実行します。
flagの値が2の場合は、judge()関数を動かさず、ただdoSomething()を実行します。
flagの値が上記以外の場合はなにもしません。
簡単な処理です。

2.実際に書いてみましょう

さて、では以降の文章を読む前に、皆さん各自実際にプログラムとしてこの処理を書いてみましょう。
チラシの裏にペンで書いてもいいし、テキストエディタに書いてもいいです。
簡単でしょ? 1分もかからないかと思います。
書いた?
ちゃんと書いた?
書いた人は下に読み進めて行きましょう。

3.答え合わせ

さて、どんな感じになりましたかね?

ちなみにこんなかんじの回答パターンを用意してみました。

private void hoge(int flag) {
    // flag = 1 or 2 or 3

    // Pattern1
    if (flag == 1) {
        if (judge()) {
            doSomething();
        }
    } else if (flag == 2) {
        doSomething();
    }

    // Pattern2
    boolean temp = false;
    if (flag == 1) {
        temp = judge();
    }
    if (temp || flag == 2) {
        doSomething();
    }

    // Pattern3
    if ((flag == 1 && judge()) || flag == 2) {
        doSomething();
    }

    // Pattern4
    switch (flag) {
    case 1:
        if (!judge()){
            break;
        }
    case 2:
        doSomething();
    default:
        break;
    }
}

細かいバリエーションは抜かして思いつく限りのパターンを書いてみたのですが、皆さんの回答はどれかに当てはまってでしょうか?
動作確認はしたので、正しくは動くかと思います。

4.どれが好き?

上に挙げた4つのパターンはそれぞれ長所と短所のようなものがあります。
読みやすいかどうかとか、正規化されている(処理が冗長でない)かとか、汎用性があるかとか。

まず、パターン1はそれぞれのflagの値による動作が追いやすいです。
プログラムの中でflagが処理制御用の変数であるならば、それ毎に処理が追えたほうが良いという考え方もあります。
ただし、見て分かる通りdoSomething()が2回書かれており、無駄があります。あと「要するにflagが2の時はjudge()しなくいい」という見方はしにくいですね。

次に、パターン2は処理に無駄はありませんが、パッと見た感じがわかりにくいかなと思います。何より一度判定結果をboolean変数に代入しているので、直感的でなくなってしまっています。

パターン3も処理に無駄はありませんし、行数も少なく、見た感じは一番スマートですね。
ただ、一行に固めて書いた影響で、flagとなっている変数名が長い名前に変更になった場合などに見難くなる可能性が高いです。judge()に関しても、本当はもっと長い関数名にするでしょうし、実際にはflagはintではなくStringでequals()を使った判定をするかもしれません。例えば、

// Pattern3
if ((myMostFavoriteArtist.equals("ももいろクローバーZ") && judgeDoSomethingRunning()) || (myMostFavoriteArtist.equals("私立恵比寿中学") ) {
    doSomething();
}

みたいな。

パターン4はswitch文を使った用法で、直感的に理解しやすいかもしれません。
ただ、このプログラムを理解するには「javaのswitch文がラベルジャンプ的に動作していて、breakしない場合はどんどん下の行を実行していく」ということを知っている必要があります。switch文は一般的(?)には、case毎に処理を記述してbreakするような用法が多く、このことを理解していない人は意外に多いです。プログラミング言語によってはこういうふうになってないものもあった気がします。
また、javaのswitch文はswitch条件に入れられる変数の型が決まっています。私の記憶だとenumとString以外の参照変数は入れれません。実数も入れれません。条件も必ず一致条件で、比較演算子は使えません。今回のケースは「int型の一致判定」だったので、switch文を使えましたが、言語的にswitch文は案外制約が多いのです。

5.まとめ

分岐処理はいろんな書き方があって、それぞれ長所短所があって、適切に使いわけなければいけません。
コードは皆の共同所有物なので、他の人のレベルに合わせる必要もあります。
設計上の思想や業務上の要件によって、どう書いたほうが見やすいか、変更に耐えうるかも変わります。
万能の回答はありませんが、各自気をつけて書いて行きましょう。
この記事が何かのヒントになれば幸いです。

Javaの配列に関するちょっとややこしい話

昨日、現場の人のソースを読んでて気になったので指摘したのがあって、それがどんなソースだったのかご紹介。
実際に修正してもらおうとしたところ、その人がしどろもどろになってたのが面白かったので。


私が見たソースはこんなかんじでした。もちろん色々端折ってます。
要するに、「分岐させた先で配列変数に値を設定して、その後に配列変数の値を参照してなんらかの処理をしたい」のです。

private void myMethod(boolean foo){

  // 分岐
  if(foo){
    // 配列変数に値を代入する
    String bar[] = {"月", "Mon" };
		
    // 変数の値を使って何かする。
    System.out.println(bar[0] + "曜日は英語で" +  bar[1] +"です");
  } else {	
    // 配列変数に値を代入する
    String bar[] = {"火", "Tue" };
		
    // 変数の値を使って何かする。 
    System.out.println(bar[0] + "曜日は英語で" +  bar[1] +"です");
  }
}


ぱっと見でわかりますが、上に書いた処理の『// 変数の値を使って何かする。』の部分は同じ処理が分岐の両方に書いてあり無駄があります。
そこで私は実装者にこのような指摘を行いました。


『// 変数の値を使って何かする。 』の部分をif文の下に出してください。


ぱぱっと実装者の人が書いたのはこちら。
分かる人はわかると思いますが、コンパイルエラーがでます。
配列変数barのスコープはif文の各分岐内で切れてしまっているため、分岐の外では使えません。

private void myMethod(boolean foo){

  // 分岐
  if(foo){
    // 配列変数に値を代入する
    String bar[] = {"月", "Mon" };
  } else {	
    // 配列変数に値を代入する
    String bar[] = {"火", "Tue" };		
  }

  // 変数の値を使って何かする。 
  System.out.println(bar[0] + "曜日は英語で" +  bar[1] +"です");
}


ここからは私が順を追って説明しました。
まず、配列変数barのスコープを変えるため、宣言をif文の前に出します。

private void myMethod(boolean foo){

  // 分岐の前に変数を宣言する
  String bar[];

  // 分岐
  if(foo){
    // 配列変数に値を代入する
    bar = {"月", "Mon" };
  } else {	
    // 配列変数に値を代入する
    bar = {"火", "Tue" };		
  }
  // 変数の値を使って何かする。 
  System.out.println(bar[0] + "曜日は英語で" +  bar[1] +"です");
}


一見妥当なように見えますが、まだコンパイルエラーがでます。
分岐処理内で行なっている配列変数への代入は、正常にコンパイルできません。


理由を説明すると、Javaでは配列を生成できるタイミングが限られています。

  1. 配列変数を宣言する時
  2. 配列をnewする時

この2つのタイミングでしか配列を生成することが出来ません。

従って、処理の途中に下のような処理を書き込んでもコンパイルエラーになります。

bar = {"月", "Mon" };


Javaでの配列は int などのプリティブ型と違いオブジェクトとして扱われます。
なので、本来newを使わずにオブジェクトを生成することができません。


「じゃあ配列変数を宣言する時に初期化する場合はなんでnewいらないの?」と言われると、Javaが特別に気を効かせてくれているとしか言えません(笑)
(Stringなんかも特別扱いされてますね)


ということで、Javaで配列を生成したい場合はこうなります。

bar[] = new String[] {"月", "Mon" };

先ほどのプログラムは正しくはこうなります。

private void myMethod(boolean foo){

  // 分岐の前に変数を宣言する
  String bar[];

  // 分岐
  if(foo){
    // 配列変数に値を代入する
    bar = new String[] {"月", "Mon" }; // newを追加
  } else {
    // 配列変数に値を代入する
    bar = new String[] {"火", "Tue" }; // newを追加
  }
  // 変数の値を使って何かする。 
  System.out.println(bar[0] + "曜日は英語で" +  bar[1] +"です");
}


Javaの配列の扱いは特殊なので、詳しい仕様を押さえてない人は
プログラミングをそれなりにやっててもちょっとつまずくのかなと思います。


以上、Javaの配列に関するちょっとややこしい話でした。