DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

エラー処理(例外処理)の基本

エラー処理(例外処理)とは、実行時に発生したエラー(発生した例外、スローされた例外)を検出し、適切な処理を行うことです。ここではエラー処理の基本を、初心者でも分かるように、できる限り詳しく説明します。

補足:この記事では、単にエラーといった場合、例外をスローするランタイムエラー(実行時エラー)のことを指します。

ここでは、次のようなメソッドについて考えます。このメソッドは、指定されたファイルを開いて、その内容をStringとして返すものです。なお、テキストファイルを読み込む方法については「文字コードを指定してテキストファイルを読み込む」で説明しています。

VB.NET
コードを隠すコードを選択
Public Shared Function ReadAllText(ByVal filePath As String) As String
    'ファイルを開く
    Dim sr As System.IO.StreamReader = System.IO.File.OpenText(filePath)
    '内容をすべて読み込む
    Dim s As String = sr.ReadToEnd()
    'ファイルを閉じる
    sr.Close()

    '結果を返す
    Return (s)
End Function
C#
コードを隠すコードを選択
public static string ReadAllText(string filePath)
{
    //ファイルを開く
    System.IO.StreamReader sr = System.IO.File.OpenText(filePath);
    //内容をすべて読み込む
    string s = sr.ReadToEnd();
    //ファイルを閉じる
    sr.Close();

    //結果を返す
    return s;
}

例外とは?

上記のコードでは、実行時にエラーが発生する可能性が十分あります。例えば、パラメータ"filePath"に存在しないファイルを指定したり、指定したファイルがロックされていたりすれば、File.OpenTextメソッドでエラーが発生します。

エラーが発生すると、例外がスローされます。スローされる例外とは、具体的には、Exceptionクラスから派生したクラス(例外クラス)のインスタンス(例外オブジェクト)です。エラーが発生した理由によってスローされる例外クラスが異なりますので、どの例外クラスがスローされたかによってエラーが発生した理由をある程度知ることができます。

実際にエラーを発生させてみましょう。例えば、Visual Studioでアプリケーションのデバッグを開始して、上のReadAllTextメソッドに存在しないファイルを指定すると、以下のようなウィンドウが表示され、プログラムが停止します。

エラーウィンドウ

このウィンドウのメッセージにある"FileNotFoundException"というのがスローされた例外です。このウィンドウによって、OpenTextメソッドで例外FileNotFoundExceptionがスローされたことが分かります。

このようにFile.OpenTextメソッドは、指定されたファイルが存在しなかった場合、FileNotFoundExceptionという例外がスローされます。さらに、OpenTextメソッドにNothing(C#では、null)を指定するとArgumentNullExceptionが、指定されたファイルに必要なアクセス許可が無かった場合はUnauthorizedAccessExceptionがスローされます。

あるメソッドを呼び出した時どのような例外がスローされる可能性があるかは、ヘルプやMSDNを調べることにより、ある程度分かります。実際にMSDNでFile.OpenText メソッドをご覧になって、どのような例外があるか確認してみてください。ただし、困ったことに、MSDNに書かれている例外がすべてではないということも頭に入れておいてください。

また、スローされた例外オブジェクトには、発生したエラーに関する様々な情報が入っています。これらの情報はデバックの役に立ちます。例えば、上図のエラーウィンドウが表示されたときに「アクション」の「詳細の表示」をクリックすると、下図のようなダイアログが表示され、スローされたFileNotFoundExceptionオブジェクトの中身を見ることができます。

詳細の表示ダイアログ

この中で、例えば、Messageプロパティで例外の説明を知ることができます。StackTraceプロパティでスタックトレースの情報を、Sourceプロパティでエラーの原因となったオブジェクトやアセンブリの名前を知ることができます。

Exceptionクラスのプロパティについてより詳しくは、MSDNの「Visual Basic の例外クラス」や「Exception クラスとプロパティ」などをご覧ください。

Try...Catchで例外をキャッチする

例外が発生すると、通常は、「アプリケーションのコンポーネントで、ハンドルされていない例外が発生しました。...」というダイアログが表示され、プログラムが終了します。例外が発生してもプログラムを終了させないようにするには、スローされた例外をキャッチ(捕捉)します。例外をキャッチすれば、例えばOpenTextメソッドでファイルが開けなかったときに、ユーザーにファイルが開けなかったことを知らせ、もう一度ファイル名を入力してもらうというようなことができます。

例外をキャッチするには、Try...Catchステートメント(C#では、try-catchステートメント)を使います。Tryブロックに例外が発生する可能性のある処理を記述し、Catchブロックに例外が発生したときに行う処理を記述します。

先程のReadAllTextメソッドを改造して、OpenTextメソッドで例外FileNotFoundExceptionがスローされた場合、これをキャッチできるようにしてみましょう。

VB.NET
コードを隠すコードを選択
Public Shared Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader

    Try
        'ファイルを開く
        sr = System.IO.File.OpenText(filePath)
    Catch ex As System.IO.FileNotFoundException
        'FileNotFoundExceptionをキャッチした時
        System.Console.WriteLine("ファイルが見つかりませんでした。")
        System.Console.WriteLine(ex.Message)
        Return Nothing
    End Try

    '内容をすべて読み込む
    Dim s As String = sr.ReadToEnd()
    'ファイルを閉じる
    sr.Close()

    '結果を返す
    Return s
End Function
C#
コードを隠すコードを選択
public static string ReadAllText(string filePath)
{
    System.IO.StreamReader sr;

    try
    {
        //ファイルを開く
        sr = System.IO.File.OpenText(filePath);
    }
    catch (System.IO.FileNotFoundException ex)
    {
        //FileNotFoundExceptionをキャッチした時
        System.Console.WriteLine("ファイルが見つかりませんでした。");
        System.Console.WriteLine(ex.Message);
        return null;
    }

    //内容をすべて読み込む
    string s = sr.ReadToEnd();
    //ファイルを閉じる
    sr.Close();

    //結果を返す
    return s;
}

上のコードでは、TryブロックにOpenTextメソッドを入れることにより、OpenTextメソッドでスローされる例外をキャッチできるようにしています。CatchステートメントではFileNotFoundExceptionを指定することで、Tryブロック内で例外FileNotFoundExceptionがスローされたときに、このCatchブロックが実行されるようにしています。

上の例ではTryブロックに1行しか記述しませんでしたが、もちろん、複数行記述することができます。

また、上の例ではCatchブロックの最後に「Return」(C#では、return)を記述しているため、メソッドがここで終了していますが、もしこれがなかったならば、そのままTry...Catchの後のコードが実行されます。上の例で言えば、ReadToEndメソッドが呼び出されることになります(ただし変数srが空のため、エラーが発生します)。

Catchブロックを追加する

上記のコードではFileNotFoundExceptionしかキャッチしていませんので、それ以外の例外が発生したときは、今まで通りプログラムが終了します。FileNotFoundException以外の例外をキャッチするには、Catchブロックを追加します。

FileNotFoundException以外に、UnauthorizedAccessExceptionがスローされた時もキャッチできるように改造してみましょう。(Try...Catchの部分のみを示します。)

VB.NET
コードを隠すコードを選択
Try
    'ファイルを開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.UnauthorizedAccessException
    'UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
End Try
C#
コードを隠すコードを選択
try
{
    //ファイルを開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。");
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.UnauthorizedAccessException ex)
{
    //UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。");
    System.Console.WriteLine(ex.Message);
    return null;
}

このように、Tryブロックの後ろにCatchブロックを幾つも追加することができます。

Catchの順番

ファイルがロックされていたとき、OpenTextメソッドは例外IOExceptionをスローします。今度はIOExceptionをキャッチしてみましょう。以下のコードでうまく行くでしょうか?

VB.NET
コードを隠すコードを選択
Try
    'ファイルを開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.IOException
    'IOExceptionをキャッチした時
    System.Console.WriteLine("ファイルがロックされている可能性があります。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.UnauthorizedAccessException
    'UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
End Try
C#
コードを隠すコードを選択
try
{
    //ファイルを開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.IOException ex)
{
    //IOExceptionをキャッチした時
    System.Console.WriteLine("ファイルがロックされている可能性があります。");
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。");
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.UnauthorizedAccessException ex)
{
    //UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。");
    System.Console.WriteLine(ex.Message);
    return null;
}

実はこのコードには間違いがあります。「Catch ex As System.IO.FileNotFoundException」(C#では、「catch (System.IO.FileNotFoundException ex)」)の箇所で、「前の catch 句はこれ、またはスーパー型 ('System.IO.IOException') の例外のすべてを既にキャッチしました。」というコンパイルエラーが発生します。

このコンパイルエラーが発生した理由は、IOExceptionのCatchブロックがFileNotFoundExceptionのCatchブロックよりも前にあるからです。このようにしてしまうと、TryブロックでFileNotFoundExceptionがスローされたとき、IOExceptionのCatchブロックでのみキャッチされ、FileNotFoundExceptionのCatchブロックではキャッチできなくなります。

なぜこのようなことが起こるのかと言うと、FileNotFoundExceptionクラスがIOExceptionクラスの派生クラスだからです。複数のCatchブロックがあるときにTryブロックで例外がスローされると、その例外の型、あるいは派生元の型を指定しているCatchを上から順番に検索し、初めに見つかったCatchブロックだけが実行されます。

よってこのコンパイルエラーを修正するには、IOExceptionのCatchブロックをFileNotFoundExceptionのCatchブロックより後ろに移動させます。なおUnauthorizedAccessExceptionはIOExceptionの派生クラスではありませんので、この位置は関係ありません。

VB.NET
コードを隠すコードを選択
Try
    'ファイルを開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.IO.IOException
    'IOExceptionをキャッチした時
    System.Console.WriteLine("ファイルがロックされている可能性があります。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.UnauthorizedAccessException
    'UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。")
    System.Console.WriteLine(ex.Message)
    Return Nothing
End Try
C#
コードを隠すコードを選択
try
{
    //ファイルを開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    System.Console.WriteLine("ファイルが見つかりませんでした。");
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.IO.IOException ex)
{
    //IOExceptionをキャッチした時
    System.Console.WriteLine("ファイルがロックされている可能性があります。");
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.UnauthorizedAccessException ex)
{
    //UnauthorizedAccessExceptionをキャッチした時
    System.Console.WriteLine("必要なアクセス許可がありません。");
    System.Console.WriteLine(ex.Message);
    return null;
}

すべての例外をキャッチする

OpenTextメソッドはこれ以外にも様々な例外をスローします。MSDNで説明されていない例外をスローする可能性もあります。

すべての例外をキャッチするには、Exceptionをキャッチします。Exceptionはすべての例外の基本クラスですので、これだけですべての例外をキャッチできます。

補足:.NET Framework 4.0からは、破損状態例外は、このようにしてもキャッチできません。破損状態例外が発生した時は、プロセスが破損状態にある可能性が高く、このまま実行し続けるのは危険なため、キャッチして無視することができないようになっています。ただし、HandleProcessCorruptedStateExceptions属性を使えば、破損状態例外をキャッチすることができます。詳しくは、「HandleProcessCorruptedStateExceptionsAttribute クラス」や「破損状態例外を処理する」をご覧ください。

なお、ExceptionのCatchブロックは一番最後に記述しなければなりませんが、その理由は前述した通りです。

VB.NET
コードを隠すコードを選択
Try
    'ファイルを開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.IO.IOException
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.UnauthorizedAccessException
    System.Console.WriteLine(ex.Message)
    Return Nothing
Catch ex As System.Exception
    'すべての例外をキャッチする
    '例外の説明を表示する
    System.Console.WriteLine(ex.Message)
    Return Nothing
End Try
C#
コードを隠すコードを選択
try
{
    //ファイルを開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.IO.IOException ex)
{
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.UnauthorizedAccessException ex)
{
    System.Console.WriteLine(ex.Message);
    return null;
}
catch (System.Exception ex)
{
    //すべての例外をキャッチする
    //例外の説明を表示する
    System.Console.WriteLine(ex.Message);
    return null;
}

Exceptionですべての例外をキャッチするのはとても便利に思われるかもしれませんが、通常、すべきではありません。

例外をキャッチする主な目的は、エラーの原因を取り除いて、回復することです。例えばFileNotFoundExceptionがスローされた時は、ユーザーにそのファイルが存在しないことを伝え、もう一度ファイル名を入力してもらうことで回復が可能です。一方、原因が不明な例外や、解決法のない例外をキャッチして、何もせずにプログラムを継続させる(例外を握りつぶす)と、問題を残したままになり、危険です。回復できない例外が発生した時は、プログラムを終了させた方が安全です。

また、予想外の例外を握りつぶすことでバグを発見しづらくなる点も重大です。

Exceptionをキャッチしても良いケースとしては、例えばエラーが発生した時にログをとってからプログラムを終了させる時のように、例外を再スローするケース(後述します)などに限られます。

ちなみに、Exceptionをキャッチして再スローしないコードをVisual Studioのコード分析機能で分析すると、「汎用的な例外の種類をキャッチしないでください」(TypeName: DoNotCatchGeneralExceptionTypes)という警告が出ます。また、MSDNの「例外のデザインのガイドライン 例外処理」でも同様のガイドラインが示されています。

例外が発生したことは知るが、例外は発生させる(例外を再スローする)

先に述べたように、例外が発生したときにログをとるが、例外はそのまま発生させたいというケースや、例外が発生したことをメッセージボックスでユーザーに知らせるが、例外は発生させたいというケースでは、「例外の再スロー」を行います。

例外の再スローを行うには、Catchブロックで例外をキャッチし、Throwステートメント(C#では、throwステートメント)で例外をスローします。

以下に例を示します。ここでは、Exceptionですべての例外をキャッチしています。例外が発生すると、コンソールに例外の説明を出力した後、キャッチした例外をそのまま再スローしています。

例外を再スローすると、キャッチされなかった例外と同様に、ReadAllTextメソッドを呼び出したコードでキャッチすることが出来ますし、どこでもキャッチされなければプログラムが終了します。

VB.NET
コードを隠すコードを選択
Try
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.Exception
    'すべての例外をキャッチする
    'コンソールに例外の説明を出力する
    System.Console.WriteLine(ex.Message)

    '例外を再スローする
    Throw
End Try
C#
コードを隠すコードを選択
try
{
    sr = System.IO.File.OpenText(filePath);
}
catch (System.Exception ex)
{
    //すべての例外をキャッチする
    //コンソールに例外の説明を出力する
    System.Console.WriteLine(ex.Message);

    //例外を再スローする
    throw;
}

Throwステートメントにはスローする例外を指定することもできますので(Catchブロックの外では、こちらが本来の使い方です)、上記のコードは下記のコードと同じと思われる方もいらっしゃるかもしれません。

VB.NET
コードを隠すコードを選択
Try
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.Exception
    'すべての例外をキャッチする
    'コンソールに例外の説明を出力する
    System.Console.WriteLine(ex.Message)

    '例外を再スローする
    Throw ex
End Try
C#
コードを隠すコードを選択
try
{
    sr = System.IO.File.OpenText(filePath);
}
catch (System.Exception ex)
{
    //すべての例外をキャッチする
    //コンソールに例外の説明を出力する
    System.Console.WriteLine(ex.Message);

    //例外を再スローする
    throw ex;
}

しかし前者と後者は異なり、前者の方法を使うべきです。

実は、前者と後者の方法では、スローされる例外のスタックトレース(StackTraceプロパティ)の内容に大きな違いが出ます。前者の方法では、例外のスタックトレースを基のまま保持します。しかし後者の方法では、再スローした位置(上の例では、ReadAllTextメソッド)からスタックトレースが開始されるため、StackTraceプロパティには再スローした場所からの情報しか残りません。

Visual Studioのコード分析機能を使うと、後者の方法では、「スタックの詳細を保存するために再スローします」(TypeName: RethrowToPreserveStackDetails)という警告が出ます。また、MSDNの「例外のデザインのガイドライン 例外処理」でも同様のガイドラインが示されています。

補足:もちろんスタックトレースの開始位置をReadAllTextメソッドにしたいというケースもあるでしょう。しかしそのような場合は、キャッチした例外をそのまま再スローするのではなく、新しい例外を作成してスローします。これについては基本の域を超えますのでここでは説明しませんが、MSDNでは「方法 : 例外を明示的にスローする」や「例外のデザインのガイドライン 例外のラップ」などが参考になるでしょう。

Exceptionオブジェクトが必要ないときに省略する

CatchブロックでExceptionをキャッチするときは、以下のように例外の型を省略できます。

VB.NET
コードを隠すコードを選択
Try
    sr = System.IO.File.OpenText(filePath)
Catch
    '全ての例外をキャッチし、再スローする
    Throw
End Try
C#
コードを隠すコードを選択
try
{
    sr = System.IO.File.OpenText(filePath);
}
catch
{
    //全ての例外をキャッチし、再スローする
    throw;
}

また、C#では、次のようにして例外の型だけを記述することもできます。

C#
コードを隠すコードを選択
try
{
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException)
{
    //FileNotFoundExceptionをキャッチする
    System.Console.WriteLine("ファイルが存在しません。");
    throw;
}

できるだけ例外が発生しないようにする

例外が発生すると多くのリソースを消費し、処理に時間がかかります。例外は、できるだけ発生しないようにすべきです。

例えば今までの例で、OpenTextメソッドに渡す文字列がNothing(C#では、null)だった時にArgumentNullExceptionがスローされますが、文字列がNothingでないことを事前に確認することは簡単ですので、ArgumentNullExceptionをキャッチするのではなく、事前に文字列がNothingでないことをチェックすることで対処すべきです。

また、ファイルの存在を確認することもFile.Existsメソッドを使えば簡単です。(File.Existsメソッドはさらに、指定された文字列がパスとして適切かや、ファイルを読み取るためのアクセス許可があるかも調べます。)以下に、File.OpenTextメソッドを呼び出す前にFile.Existsメソッドでファイルの存在を確認する例を示します。

VB.NET
コードを隠すコードを選択
Public Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader

    Try
        'ファイルが存在するか調べる
        If System.IO.File.Exists(filePath) Then
            'ファイルを開く
            sr = System.IO.File.OpenText(filePath)
        Else
            'ファイルが存在しない時
            System.Console.WriteLine("ファイルが存在しません。")
            Return Nothing
        End If
    Catch ex As System.Exception
        System.Console.WriteLine(ex.Message)
        Throw
    End Try

    Dim s As String = sr.ReadToEnd()
    sr.Close()

    Return s
End Function
C#
コードを隠すコードを選択
public string ReadAllText(string filePath)
{
    System.IO.StreamReader sr;

    try
    {
        //ファイルが存在するか調べる
        if (System.IO.File.Exists(filePath))
        {
            //ファイルを開く
            sr = System.IO.File.OpenText(filePath);
        }
        else
        {
            //ファイルが存在しない時
            System.Console.WriteLine("ファイルが存在しません。");
            return null;
        }
    }
    catch (System.Exception ex)
    {
        System.Console.WriteLine(ex.Message);
        throw;
    }

    string s = sr.ReadToEnd();
    sr.Close();

    return s;
}

ただしこのケースでは、File.Existsメソッドで事前チェックを行っても、FileNotFoundExceptionの発生を完全に防ぐことはできません。なぜならば、File.Existsメソッドでファイルのチェックを行った後、OpenTextメソッドが呼び出される前にファイルが削除されてしまう可能性があるからです。よってFile.Existsメソッドでファイルのチェックを行ったとしても、FileNotFoundExceptionをキャッチしなければならないことには変わりません。

しかし、事前のチェックによって例外が発生する可能性は大幅に減少しますので、File.Existsメソッドで事前チェックを行うのは決して無駄ではありません。

このような事前のチェックは必ず行った方が良いかと言うと、そうとも言い切れません。例外がスローされないのであれば、Try...Catchはパフォーマンスにほとんど影響を与えませんので、例外がほとんど発生しないのであれば、事前チェックによるパフォーマンスの低下の方が問題になるかもしれません。例えば、Directory.GetFilesメソッドで取得した大量のファイルをOpenTextメソッドで次々と開くのであれば、File.Existsメソッドによるチェックを省略して、Try...Catchのみとする選択も当然考えられるでしょう。

このように、その例外が発生する頻度の多さや、事前チェックにかかる時間の長さなどを考慮して、ケースバイケースで対応してください。

補足:「Do try...catch blocks hurt runtime performance?」で説明されているように、一般的には、例外をスローしないのであれば、、Try...Catchはパフォーマンスを低下させないと言われています。しかし、「Performance Implications of try/catch/finally」によると、Try...CatchはJITコンパイラの最適化に影響を与えるため、その結果としてパフォーマンスを低下させる可能性があるようです。

File.ExistsメソッドがTryブロック内に入っている点も説明しておきましょう。MSDNの「File.Exists メソッド」ではこのような方法を推奨しており、その理由として、競合が発生する可能性のある範囲を狭めることができる(つまり、Existsメソッドでファイルの存在をチェックしてからOpenTextメソッドでファイルを開くまでの間にファイルが削除されてしまう可能性を減らすことができる)からと説明しています。しかしこのような特別な事情がないのであれば、Tryブロックの範囲を狭くするために、File.Existsメソッドの呼び出しをTryブロックの外に出した方が良いでしょう。

補足:MSDNでのFile.Existsメソッドの上記のような説明は、.NET Framework 3.5までは存在しましたが、4.0からはなくなりました。

Tester-DoerパターンとTryParseパターン

例外のデザインのガイドライン 例外とパフォーマンス」では、例外をなるべく発生させない方法として、Tester-DoerパターンとTryParseパターンの使用を勧めています。

Tester-Doerパターンとは、まさに上で説明したような方法です。例外をスローさせる可能性のあるメンバを呼び出す前に、例外がスローされる状態にないかをテストする方法です。

TryParseパターンとは、例えばDouble型などのParseメソッドを呼び出す場合、その代わりに、失敗しても例外をスローしないTryParseメソッドを呼び出すという方法です。TryParseメソッド以外にも、IDictionary(TKey, TValue).TryGetValueメソッド(ディクショナリにないキーで要素を取得しようとしても例外が発生しない)やUri.TryCreateメソッド(Uriが作成できなくても例外が発生しない)などのように、同様の役割を持つメソッドもあります。

確実にCloseメソッドを呼び出す

最後にFinallyステートメントについて触れておきます。これは今までとはちょっと違う話になります。

Try...Catchステートメントには、その最後にFinallyブロック(C#では、finallyブロック)を記述することができます。Finallyブロックは、Tryブロックがどのように終了したかにかかわらず、必ず実行されます。つまり、Tryブロックが正常に終了したときは言うまでもなく、Tryブロックで例外が発生してプログラムが終了したときでも実行されます。よってFinallyブロックには、開いたファイルを閉じる処理や、リソースを解放する処理などのように、最後に必ずやらなければならない処理を記述するのが一般的です。

補足:正確に言うと、Finallyブロックは必ず実行されるとは言い切れません。詳しくは、「Finally文が実行されないケースはあるか?」をご覧ください。

例えば今までの例では、ReadToEndメソッドで例外が発生すると、Closeメソッドが呼び出されません。これを防ぐには、ReadToEndメソッドをTryブロックに入れ、FinallyブロックでCloseメソッドを呼び出します。

VB.NET
コードを隠すコードを選択
Public Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader = Nothing
    Dim s As String = Nothing

    Try
        sr = System.IO.File.OpenText(filePath)
        '内容をすべて読み込む
        s = sr.ReadToEnd()
    Catch ex As System.Exception
        System.Console.WriteLine(ex.Message)
        Return Nothing
    Finally
        '確実にファイルを閉じる
        If Not sr Is Nothing Then
            sr.Close()
        End If
    End Try

    Return s
End Function
C#
コードを隠すコードを選択
public string ReadAllText(string filePath)
{
    System.IO.StreamReader sr = null;
    string s = null;

    try
    {
        sr = System.IO.File.OpenText(filePath);
        //内容をすべて読み込む
        s = sr.ReadToEnd();
    }
    catch (System.Exception ex)
    {
        System.Console.WriteLine(ex.Message);
        return null;
    }
    finally
    {
        //確実にファイルを閉じる
        if (sr != null)
        {
            sr.Close();
        }
    }

    return s;
}

もし例外をキャッチする必要がなくFinallyブロックのみを使いたいのであれば、Catchブロックを記述せずに、TryとFinallyだけにします。さらにこの場合、Usingステートメント(C#では、usingステートメント)を使うと、さらにシンプルになります。詳しくは、「Dispose、Closeが確実に呼び出されるようにする」で説明しています。

まとめ

最後に、簡単にまとめます。詳しくは、上記の説明をお読みください

  • エラー処理(例外処理)を行うには、Try...Catchステートメント(C#では、try-catchステートメント)を使う。
  • ある例外と、その基本クラスの例外の両方をキャッチするには、基本クラスの例外のCatchブロックを必ず後に置く。
  • Exceptionを使えばすべての例外をキャッチできるが、再スローしないのであれば、すべきでない。回復できない例外は握りつぶさない。
  • キャッチした例外をそのまま再スローする場合は、引数のないThrowステートメント(C#では、throwステートメント)を使う。
  • 例外の発生はできるだけ抑える。
  • Finallyブロック(C#では、finallyブロック)に、最後に必ず行うべき処理を記述することができる。ただし、例外をキャッチしないのであれば、TryとFinallyだけにするか、Usingステートメント(C#では、usingステートメント)を使う。

以上を基に、例外処理を施したReadAllTextメソッドの例を示します。

VB.NET
コードを隠すコードを選択
Public Shared Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader = Nothing
    Dim s As String = Nothing

    Try
        '例外をキャッチしたい処理をTryブロックに入れる

        '例外をできるだけ発生させないように、事前にチェックを行う
        If System.IO.File.Exists(filePath) Then
            sr = System.IO.File.OpenText(filePath)
            s = sr.ReadToEnd()
        Else
            System.Console.WriteLine( _
                "ファイル '{0}' が見つかりませんでした。", filePath)
        End If
    Catch ex As System.IO.FileNotFoundException
        'TryブロックでFileNotFoundExceptionが発生すると実行される
        System.Console.WriteLine( _
            "ファイル '{0}' が見つかりませんでした。", filePath)
        System.Console.WriteLine(ex.Message)
    Catch ex As System.IO.IOException
        'IOExceptionのCatchブロックは必ずFileNotFoundExceptionより後に記述する
        System.Console.WriteLine( _
            "ファイル '{0}' がロックされている可能性があります。", filePath)
        System.Console.WriteLine(ex.Message)
    Catch ex As System.UnauthorizedAccessException
        System.Console.WriteLine( _
            "ファイル '{0}' へのアクセス許可がありません。", filePath)
    Catch ex As System.Exception
        'ExceptionのCatchブロックは必ず最後に記述する
        'すべての例外をキャッチするならば、最後に再スローする
        System.Console.WriteLine(ex.Message)
        '例外をそのまま再スローするならば、スローする例外を指定しない
        Throw
    Finally
        '必ず実行させる処理をFinallyに記述する
        If Not sr Is Nothing Then
            sr.Close()
        End If
    End Try

    '結果を返す
    Return s
End Function
C#
コードを隠すコードを選択
public static string ReadAllText(string filePath)
{
    System.IO.StreamReader sr = null;
    string s = null;

    try
    {
        //例外をキャッチしたい処理をtryブロックに入れる

        //例外をできるだけ発生させないように、事前にチェックを行う
        if (System.IO.File.Exists(filePath))
        {
            sr = System.IO.File.OpenText(filePath);
            s = sr.ReadToEnd();
        }
        else
        {
            System.Console.WriteLine(
                "ファイル '{0}' が見つかりませんでした。", filePath);
        }
    }
    catch (System.IO.FileNotFoundException ex)
    {
        //tryブロックでFileNotFoundExceptionが発生すると実行される
        System.Console.WriteLine(
            "ファイル '{0}' が見つかりませんでした。", filePath);
        System.Console.WriteLine(ex.Message);
    }
    //IOExceptionのcatchブロックは必ずFileNotFoundExceptionより後に記述する
    catch (System.IO.IOException ex)
    {
        System.Console.WriteLine(
            "ファイル '{0}' がロックされている可能性があります。", filePath);
        System.Console.WriteLine(ex.Message);
    }
    //例外クラスのインスタンスが必要なければ、次のように省略できる
    catch (System.UnauthorizedAccessException)
    {
        System.Console.WriteLine(
            "ファイル '{0}' へのアクセス許可がありません。", filePath);
    }
    //Exceptionのcatchブロックは必ず最後に記述する
    catch (System.Exception ex)
    {
        //すべての例外をキャッチするならば、最後に再スローする
        System.Console.WriteLine(ex.Message);
        //例外をそのまま再スローするならば、スローする例外を指定しない
        throw;
    }
    finally
    {
        //必ず実行させる処理をfinallyに記述する
        if (sr != null)
        {
            sr.Close();
        }
    }

    //結果を返す
    return s;
}

ここで説明しなかった事柄

この記事では、これだけ知っておけばエラー処理の基本は十分だろうと思われる事柄をすべて説明したつもりです(もしこれ以外に必要だと思われる事柄がありましたら、コメントで教えいただければ幸いです)。

ここで説明しなかった事柄としては、VB.NETのCatchステートメントに付けるWhen句や、「On Error GoTo」を使った非構造化例外処理がありますが、これらは知っておく必要はないだろうと判断しました。

また、Try...Catchのネスト(Tryブロックの中にTry...Catchを入れること)についても説明しませんでしたが、簡単にいえば、Try...Catchがキャッチしなかった(あるいは再スローした)例外を、その外のTry...Catchでキャッチできるというだけのことです。

ここで説明しなかった事柄について興味のある方は、お手数ですが、MSDNなどでお調べください。

  • 履歴:
  • 2010/2/26 ガイドラインに沿った内容になるように、大幅に書き直した。
  • 2013/6/20 破損状態例外についての記述を追加。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。