あらゆるところで否定され続けているJavaの検査例外だが、なぜこのような仕組みが必要なのか説明しようと思う。
この投稿は golang で複数のエラーをハンドリングする方法 という記事を見かけたので書いた。
いつも通り「コメントは大歓迎」です。
検査例外の話
検査例外の目的は 静的な分岐の強制 である。
例えばこのようなコードがあったとする。
f = File.open("dummy.txt")
f.write "Hello"
f.close
このようなAPIではプログラマが「ファイルを開けなかったときの処理を忘れる」ことを防ぐことができない。
一方、検査例外を使えば「開けなかったときの処理」をプログラマが忘れることができない。
忘れた場合はコンパイルエラーになり、そもそも実行できないからである。
(簡単のためRuby風なAPIにしている)
try {
File f = File.open("dummy.txt");
f.write("Hello");
f.close();
} catch (IOException e) { // catchしない場合コンパイルエラー
System.out.println("ファイルを開けませんでした。");
}
Maybe/Eitherの話
検査例外というのは、関数型言語におけるMaybe
やEither
の一種だと考えるのが良い。
型システムを利用して、失敗したときの処理を書き忘れるのを防ぐのが目的である。
Scala風なプログラムで、分岐を強制している例を挙げる。
val result = File.open("dummy.txt")
result match {
case Success(f) => f.write("Hello")
case Fail => System.out.println("ファイルを開けませんでした。")
}
このように分岐後でしか f
に触れることができず、分岐を強制させることができる。
Kotlinの話
Kotlinでは「null
になる可能性がある変数」かそうでないかを明確に区別する。 参考
最近Facebookが発表したHackにも同様の機能があるようだ。
val file : File? = File.open("dummy.txt") // 型は明示的に書いているだけで省略可能
file.write("Hello") // コンパイルエラー
// OKな例
val file : File? = File.open("dummy.txt")
if (file != null) {
file.write("Hello") // ここのfileの型は<File?>ではなく<File>
} else {
print("ファイルを開けませんでした。");
}
通常の言語において、変数 file
の型はずっと変わらないが、この言語ではifチェックの前後で型が異なるように振る舞う。
Goの話
(断わっておくが、Goのことはよく知らないので、間違いがあればコメントがほしい)
if file, err := file_open("dummy.txt"); err != nil {
print("ファイルを開けませんでした。");
} else {
file.write("Hello");
}
// 無視する例
file, _ := file_open("dummy.txt");
file.write("Hello");
Goは失敗時に二値を返すという方針のようだ。
値が二つある以上、「分岐を忘れる」ことは少なくなるが、強制はできないだろう。
また、正常系でも異常系でもfile
, err
に触れるのも問題である。
C#の話
これはC#でも同様である。C#では例外以外にはTryXxx
を使うのが推奨されている。
File file;
if (File.TryOpen("dummy.txt", out file)) {
file.Write("Hello");
} else {
Console.WriteLine("ファイルを開けませんでした。");
}
// 無視する例
File file;
File.TryOpen("dummy.txt", out file);
file.Write("Hello");
関数は bool
を返すため、分岐を忘れることは少なくなるが、強制できるわけではない。
まとめ
検査例外はそんなに悪い機能じゃない
コンパイルエラーを有効に使おう