エラー処理。プログラミングの学習中は、なかなか関心が持たれない。だけど実際には、とても大事な必須の処理。
エラー処理は、どのような考え方で作るべきなのでしょうか?
エラー処理ではなにをするのか?
想定外の事象に対処する
プログラムでは、動作の途中でしばしば、普段では起こらない事象に遭遇します。
例えばお買い物サイトプログラムなら
- 商品購入しようとおもったら、ちょうど売り切れになっていた…
- 購入決算のためにサーバに接続しようとしたら、つながらない…
- なぜか分からないが、突然プログラムが途中で終わった…
このような、通常の動作と異なる挙動に対処するのが、エラー処理です。
エラー処理がなければ、
- 売り切れになってるのに、買い物手続きが進んでしまう
- 途中で処理中断したのに、金額清算されてしまう
といった事態が起こります。
これを防ぐために、エラー処理が必須なわけです。
エラー処理の、大事な3つの考え方
エラー処理のプログラミングで重要なのは、以下の3つの考え方。
- エラーが発生したら、すみやかに中断する
- エラーが発生した時点で、リソースを確実に開放する
- 状況分析のため、エラー情報を記録する
大事なこと(1)すみやかに中断する
プログラムは、エラーが発生したら、正常の処理を中断して、異常時の処置に移る必要があります。
エラーが発生した時点で、即時に中断する。大切です。
エラー状態のまま処理を進めてはいけません。商品が売り切れになったのに決済画面に進むのは、おかしいですよね。
「トラッシュよりクラッシュ」という言葉があります。エラーのまま混迷な状態に陥るよりは、その場で停止したほうがまだマシだ、という意味です。
エラーを無視せず、すみやかに対処しましょう。
大事なこと(2)リソースを確実に開放する
リソースとは、プログラムが使うなにかしらの材料です。例えばこんなものがあります。
- メモリ
- ファイル
- ネットワーク
- データベース
リソースは、使うときに確保し、使い終わったら開放する必要があります。そうしないと次の処理が使えません。
エラーの場合は処理中断するので、気を付けないとリソースを確保したまま開放されない状態になってしまいます。それが更に深刻なエラーを引き起こしかねません。
正常の場合と同様に、エラーの場合でも、リソースの開放は確実に行う必要があります。
大事なこと(3)エラー情報を記録する
エラーが発生したとき、その原因がすぐには分からないことがあります。
あとでエラー原因を分析するため、なにが起こったかを記録しておく必要があります。
この記録のことを、ロギングといいます。たいていの場合、エラー情報をユーザーに見えないところにテキストファイルで残します。このファイルをログファイルといいます。
3つの考え方をプログラミングする
この3つの考え方、実際にどうプログラミングするのか、押さえておきましょう。
プログラミングでの、エラー処理の実装方法
エラー処理には、以下の2つが必要です。
- エラーが発生したことを通知する
- エラーに対して何か対処する
モジュール分割されたプログラムでは、この2つを、それぞれ別のモジュールで対処することが多いです。
実装方法は大きく分けて2通り。
- 戻り値方式 戻り値リターンでエラーを伝え、リターンしたモジュールでエラー対処
- 例外方式 例外スローでエラーを伝え、例外キャッチしたモジュールでエラー対処
- 戻り値方式を取るのは、C、Go、Rustなど
- 例外方式は多くの言語が採用しています。C++、Java、Python、Ruby、PHP、JavaScript、Kotlin、Swift…
プログラミング言語のエラー処理は、歴史的に、戻り値方式(C言語)から始まり例外方式(C++,Java…)が主流になり、また戻り値方式に戻る(Go,Rust)という歴史があります。ここでは、主流の例外方式で説明します。
Javaでの、例外のプログラム例です。
public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
// 例外が発生する可能性があるコードを記述する
int result = divide(10, 0);
System.out.println("結果: " + result);
} catch (ArithmeticException e) {
// 発生した例外がArithmeticExceptionの場合の処理
System.out.println("0での割り算はできません。");
}
}
public static int divide(int num1, int num2) {
// 引数の検証
if (num2 == 0) {
throw new ArithmeticException("ゼロでの割り算はできません。");
}
// 割り算の実行
return num1 / num2;
}
}
エラー処理でなにをするのか、順番に見ていきましょう。
エラー処理実装(1)throwで、すみやかに中断する
エラーが発生したときは、throwで例外を発生させます。(例外を投げる、といった言い方もします)
throwが実行されると、次の処理には進まず、catchまで処理が飛びます。そのメソッド内にcatchがなければ、呼び出し元までさかのぼっていきます。
エラー処理実装(2)-1 finallyでリソース開放
多くのプログラミング言語では、リソース開放を確実に行うための手段を準備しています。
やりかたは大きく2種類。
ひとつめは、finally で確実にリソースを開放する方法です。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FinallyExample {
public static void main(String[] args) {
FileInputStream file = null;
try {
file = new FileInputStream("example.txt");
// ファイルを読み込む処理などを記述
} catch (FileNotFoundException e) {
System.out.println("ファイルが見つかりませんでした。");
} finally {
// ファイルのクローズ処理など、リソースの解放を行う
if (file != null) {
try {
file.close();
} catch (IOException e) {
System.out.println("ファイルのクローズ中にエラーが発生しました。");
}
}
}
}
}
catchブロックは、throwが発生した場合のみ動きます。ここでエラー処理を行います。
finallyブロックは、throwが発生してもしなくても、必ず動きます。
ここにリソース開放処理を入れることで、正常の場合でもエラーの場合でも、必ずリソースを開放できるようになります。
エラー処理実装(2)-2 try-with-resourceでリソース開放
もうひとつのリソース開放方法は、try-with-resourceと呼ばれる方法です。
tryブロックで、そのブロック中で確保するリソースを宣言すると、ブロック終了時に必ず開放します。
tryブロックが終わるのは、ブロック内の最後の処理が終わった時、もしくはthrowでブロックを抜けるときです。
このどちらの場合であっても、tryブロックを抜けたタイミングで必ずリソースを開放します。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (FileInputStream file = new FileInputStream("example.txt")) {
// ファイルを読み込む処理などを記述
} catch (FileNotFoundException e) {
System.out.println("ファイルが見つかりませんでした。");
} catch (IOException e) {
System.out.println("ファイルの読み込み中にエラーが発生しました。");
}
}
}
プログラミング言語ごとのリソース開放方法
リソース自動開放の仕掛けは、プログラミング言語によって、実装方法にかなりの違いがあります。
基本的なプログラミング構文は、どの言語もけっこう似てくるものですが、このリソース自動開放はほんとうにバリエーション豊か。
以下に一例をあげます。
- C++ RAII
- Java try-with-resource
- C# using
- Python with
- Swift defer
- Kotlin usr
- Go defer
- Rust Dropトレイト
エラー処理実装(3)ロギングライブラリによるエラー情報を記録
ログの出力は、一般的にはテキストファイルへのエラー情報の書き込みによって実現します。
ロギング処理(ログファイルへの出力)を、自前で実装することは避けましょう。
自前での実装は、以下のような問題があります。
- エラー発生時の確実な書き込みができないときがある
- 並列処理(マルチスレッドなど)のときに競合書き込みでクラッシュする恐れがある
必ずロギングライブラリを使いましょう。Javaであれば、log4j が有名です。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ExceptionHandlingSample {
private static final Logger logger = LogManager.getLogger(ExceptionHandlingSample.class);
public static void main(String[] args) {
try {
// 例外が発生する可能性がある処理
divide(10, 0);
} catch (ArithmeticException e) {
// 例外が発生した場合の処理
logger.error("Divide by zero error", e);
}
}
public static int divide(int dividend, int divisor) {
if (divisor == 0) {
throw new ArithmeticException("Divide by zero");
}
return dividend / divisor;
}
}
ロギング内容で、なによりまず大事なのは「エラーが発生した箇所を特定できること」、次に「エラー発生までの過程を追えること」です。
プログラミング言語ごとのスタックトレース取得方法
エラー発生時の過程を追うには、スタックトレースが便利です。スタックトレースで、エラー発生時に以下のことが分かります。
- エラーが発生した場所(ファイル名、行番号、関数名やメソッド名など)
- そのエラーを引き起こした関数やメソッドの呼び出し履歴
- 各関数やメソッドの呼び出しに関連する情報(引数の値など)
多くのプログラミング言語で、エラー発生時にスタックトレースが自動的に収集されているので、それを利用できます。
- Java ExceptionクラスのprintStackTraceやgetStackTrace
- C# ExceptionクラスのStackTraceプロパティ
- Python tracebackモジュール
- JavaScript Errorオブジェクトのstack
- Swift Thread.callStackSymbols
- Kotlin ExceptionクラスのprintStackTraceやgetStackTrace
- Go runtime.stack、debug.PrintStack
- Rust std::backtrace::Backtrace
意識的にログ出力処理を入れましょう。
まとめ
エラー処理は、どうしても面白味には欠けるので、軽視しがちなところがあります。
しかし、プログラムの健全性を保つために、なくてはならないものです。エラー処理の基本を押さえて、あなたのプログラムを頑健なものにしましょう。
コメント