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

Status Code 303 - See Other

Java, C, C++, C#, Objective-C, Swift, bash, perl, ruby, PHP, Python, Scala, Groovy, Go, DevOps, Raspberry Pi など。情報の誤りや指摘・意見などは自由にどうぞ。

if-else 文と条件演算子(?:)の使い分け

概要

プログラミングを行ううえで、複数の選択があり基本的にどちらでも実装できる処理は数多くある。
これらは常にどっちを使うではなく、これらはプログラムが対象とする処理の内容に応じて使い分けるべきである。

今回は、その中でも分岐を表す方法として if-else 文と条件演算子の使い分けについて記述する。

詳細説明

記述法の違い

ここでは、ある条件に応じて、文字列変数 val を分岐させる処理を記述する。

if-else文
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = null;
	if(condition) {
		// 真の場合の処理
		val = "真の場合の値";
	}else {
		// 偽の場合の処理
		val = "偽の場合の値";
	}
条件演算子
	boolean condition = true; // ここに処理を分ける基準(条件式)
	string val = condition ? "真の場合の値" : "偽の場合の値"; // 値を分岐

それぞれの利点/欠点

if-else 文

  • void 型命令も記述可能
  • 処理を複数記述できる

条件演算子

  • 乱用しなければ読みやすい
  • 事前に変数宣言する必要がなし(変数が冗長にならない)

どっちを使うべきか?

if-else 文は処理を分岐させることができ、条件演算子は値を分岐させるために用いる。
そして、一般的には if-else 文の方が値の分岐に関わらず処理も記述できるためできる事が多い。

逆に 条件演算子にできないことは、値に応じた処理内容が変わるとき。
ここから、一つの使い分け条件ができる。

条件演算子
データを選択する必要があるが、概念的にどの分岐があっても処理が同じ
if-else 文
分岐によって、全く異なる処理を選択

例1(条件演算子を使うべきだと思う例)

使用している Linux OS から使用すべきパッケージマネージャを判別し、
パッケージをインストールさせようとしているとする。
今回は CentOS/RedHat系なら rpm、そうでなければ apt-get を利用するとする。

if-else文の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = null;
	if(rpmUses.contains(getOsName())) {
		pkgmngr = "rpm -ivh";
	}else {
		pkgmngr = "apt-get install";
	}
	installPackage(pkgmngr, "zabbix-server-mysql");
条件演算子の場合
	List<String> rpmUses = Arrays.asList("centos", "redhat");
	String pkgmngr = rpmUses.contains(getOsName()) ? "rpm -ivh" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

これらは、パッケージマネージャのプログラム名(文字列データ)を選択する責任を担っている。
概念的にやってることはどの分岐でも同じなので、後者の条件演算子で記述すべきと思う。

もし、RedHat の場合は別のパッケージマネージャ、例えば yum を使うことになったら、下記のようになる。

	List<String> rpmUses = Arrays.asList("centos");
	List<String> yumUses = Arrays.asList("redhat");
	String os = getOsName();
	String pkgmngr = rpmUses.contains(os) ? "rpm -ivh" : 
			 yumUses.contains(os) ? "yum install" : "apt-get install";
	installPackage(pkgmngr, "zabbix-server-mysql");

ここで、ネストするときは行を分けて記述すると処理内容が分かりやすい。
(よく条件演算子は可読性が悪いというけれど、書式を工夫すれば問題ないと思う)
しかしこれ以上増やすと見苦しいソースコードになるので、3~4個くらいが限界か。

それ以上の分岐を書くときは、列挙型などを使用して記述する方が良い。
試しに、fedora のときは dnf というパッケージマネージャを利用することにすると。
私なら、処理内容は次のように修正する。

まず、各 OS の列挙型に使用するパッケージマネージャ情報を定義。

private static enum LinuxOS {
	CENT_OS("centos", "rpm -ivh"),
	RED_HAT("redhat", "yum install"),
	FEDORA("fedora", "yum install"),
	OTHER("other", "apt-get install");
	
	private String os;
	private String pkgmngr;
	private LinuxOS(String os, String pkgmngr) {
		this.os = os;
		this.pkgmngr = pkgmngr;
	}
	static LinuxOS get(String osName) {
		for(LinuxOS linux : values()) {
			if(linux.os.equals(osName)) {
				return linux;
			}
		}
		return OTHER;
	}
}

パッケージマネージャを取得するロジック。

	String pkgmngr = LinuxOS.get(getOsName()).pkgmngr;
	installPackage(pkgmngr, "zabbix-mysql");

ロジック自体の処理が見辛くなることはなくなるし、今後新しいパッケージマネージャが増えても列挙型を増やすだけで対応可能。
また、OS で使用するパッケージマネージャを変更したければ、OS の列挙型の中身を修正するだけで対応できる。

例2 (if文を使うべきだと思う例)

ユーザログイン画面でパスワード認証するページを想定する。
認証に成功すればメンバーページに遷移し、逆に失敗すれば認証ページを再び表示する。

if-else文の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
			gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
			gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
条件演算子の場合
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

この例では、認証失敗・成功で表示ページを切り替えるだけであるから、データの分岐に見えるかもしれない。
しかし、個人的には、この場合はif文で実装にすべきだと思っている。

なぜなら、分岐による処理の内容は本質に異なるから。
成功時に処理したい内容、認証失敗時に処理したい内容、これらは当然異なる処理になりやすい。

例えば、セキュリティ対策の一環として、あまりにログイン処理失敗が多い場合はログに記録しておきたい場合もあるし、
ログインに成功すれば、その場でセッションを新しくする処理も必要になる。
これらは、処理が本質的に異なるため条件演算子で対応すると複雑になりやすい。

以下は、仮に認証失敗時にログを取得する場合をさきほどの実装から追加した場合。

	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(pwd.equals(getPassword(id))) {
		gotoNextPage("http://www.xxxxxxxxxx.com/member");
	}else {
		writeLog(new Date() + " id->" + id); // 一行追加
		gotoNextPage("http://www.xxxxxxxxxx.com/login");
	}
	String id = "フォームから入力";
	String pwd = "フォームから入力";
	if(!pwd.equals(getPassword(id))) {
		writeLog(new Date() + " id->" + id); // 認証失敗時、結局if文を使う
	}
	String nextUrl = pwd.equals(getPassword(id)) ? "http://www.xxxxxxxxxx.com/member" : "http://www.xxxxxxxxxx.com/login";
	gotoNextPage(nextUrl);

おまけに条件演算子の実装は、失敗時に次に遷移するページを表示する前に
ログを出力するということが分かり辛いと思う。(前者に比べて)

この処理は端的に言うと「ログインが成功したときは~~、失敗したなら~~。」
というログインが成功したかが基準となり、どちらかの処理が選択される構造。
これを端的に表現できるのは、if文の強みでもある。

まとめ

条件演算子はダメで if-else 文のみという現場もあるらしいが、ナンセンスな話だと思う。
たしかに条件演算子は書き方によっては複雑になるが、フォーマットを工夫すれば見やすくなる。
なにより、分岐が多くなれば条件演算子に関わらず、見辛くなるのは if-else 文もあまり変わらない。

今回は if-else 文と条件演算子の違いについて記述したが、これらですら書き方一つでコードの読みやすさは変わる。
文法レベルだけの解釈だけでなく、現在書いている処理の意味に応じて柔軟に対応できた方が良いだろう。
このようなことでも、意識してプログラミングできる/できないが、プログラマとして実力差につながると思う。

謝辞

これらの記事を書くきっかけを作ってくれた、H氏。いつもいい刺激を頂きありがたい限り。