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

DOBON.NET

エラー処理(例外処理)を行う

エラー処理(例外処理)とは、エラーが発生した時にそれを検出し、適切な処理を行うことです。ここでは具体例を示して、エラー処理の基本を説明します。

ここでは、次のようなメソッドについて考えます。このメソッドは、ファイルを開いて、その内容をStringオブジェクトとして返すものです。なお、このコードについて詳しくは、こちらをご覧ください。

[VB.NET]
Public Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
    '内容をすべて読み込む
    Dim s As String = sr.ReadToEnd()
    '閉じる
    sr.Close()

    '結果を返す
    Return s
End Function
[C#]
public string ReadAllText(string filePath)
{
    System.IO.StreamReader sr;
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
    //内容をすべて読み込む
    string s = sr.ReadToEnd();
    //閉じる
    sr.Close();

    //結果を返す
    return s;
}

Try...Catchですべての種類の例外をキャッチする

上記のコードでは、エラーが発生する可能性が十分あります。例えば、File.OpenTextメソッドに存在しないファイルを指定すると、エラーが発生しますし、その他の理由によってもエラーが発生する可能性があります。File.OpenTextメソッドを呼び出した時にエラーが発生しても、エラーを出さずに、String.Empty("")を返すようにするには、次のようにします。

[VB.NET]
Public Function ReadAllText(ByVal filePath As String) As String
    Dim sr As System.IO.StreamReader
    Try
        'filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath)
    Catch ex As Exception
        '例外をキャッチした時
        '例外を説明するメッセージを表示
        Console.WriteLine(ex.Message)
        '空の文字列を返す
        Return String.Empty
    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
    {
        //filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath);
    }
    catch (Exception ex)
    {
        //例外をキャッチした時
        //例外を説明するメッセージを表示
        Console.WriteLine(ex.Message);
        //空の文字列を返す
        return string.Empty;
    }
    //内容をすべて読み込む
    string s = sr.ReadToEnd();
    //閉じる
    sr.Close();

    //結果を返す
    return s;
}

File.OpenTextメソッドを呼び出した時にエラーが発生すると、例外が発生します(スローされます)。上記のようにTry...Catchを使うことにより、この例外をキャッチ(捕捉)することができます。

上の例の場合、TryブロックにOpenTextメソッドを入れることにより、OpenTextメソッドで発生する例外をキャッチするようにしています。Tryブロック内で例外が発生すると、Catchブロックに制御がジャンプし、実行されます。この時、例外の情報をExceptionオブジェクトとして取得することができます。

Exceptionオブジェクトには、問題の解決に役立つ様々な情報が入っています。Messageプロパティには例外に関する説明が、Sourceプロパティにはエラーの原因となったオブジェクトやアセンブリの名前が、StackTraceプロパティにはスタックトレースの情報が入っています。さらに詳しくは、MSDN等でExceptionクラスをお調べください。

上記の例では、例外をキャッチすると、例外の説明を表示して、空の文字列を返しています。

できるだけエラーが発生しないようにする

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

例えば上記の例で、OpenTextメソッドを呼び出す前にファイルが存在しているかを調べることは、ごく簡単です。事前に調べることにより、例外が発生する機会をかなり減らすことができます。

[VB.NET]
Public Function ReadAllText(ByVal filePath As String) As String
    'ファイルが存在しているか調べる
    If Not System.IO.File.Exists(filePath) Then
        Return String.Empty
    End If

    Dim sr As System.IO.StreamReader
    Try
        'filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath)
    Catch ex As Exception
        '例外をキャッチした時
        '例外を説明するメッセージを表示
        Console.WriteLine(ex.Message)
        '空の文字列を返す
        Return String.Empty
    End Try
    '内容をすべて読み込む
    Dim s As String = sr.ReadToEnd()
    '閉じる
    sr.Close()

    '結果を返す
    Return s
End Function
[C#]
public string ReadAllText(string filePath)
{
    //ファイルが存在しているか調べる
    if (!System.IO.File.Exists(filePath))
    {
        return string.Empty;
    }

    System.IO.StreamReader sr;
    try
    {
        //filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath);
    }
    catch (Exception ex)
    {
        //例外をキャッチした時
        //例外を説明するメッセージを表示
        Console.WriteLine(ex.Message);
        //空の文字列を返す
        return string.Empty;
    }
    //内容をすべて読み込む
    string s = sr.ReadToEnd();
    //閉じる
    sr.Close();

    //結果を返す
    return s;
}

ただしこのようにしても、ファイルが存在しないことによるエラーがOpenTextメソッドで発生する可能性があることに注意してください。File.Existsメソッドでファイルのチェックを行った後、OpenTextメソッドが呼び出される前にそのファイルが削除される可能性があります。

特定の種類の例外をキャッチする

例外には様々な種類があり、エラーが発生した理由により、スローされる例外も異なります。

例えばFile.OpenTextメソッドでは、指定されたファイルが存在しなかった場合は、FileNotFoundExceptionという例外がスローされます。OpenTextメソッドにNULLが指定された場合はArgumentNullExceptionが、必要なアクセス許可が無かった場合はUnauthorizedAccessExceptionがスローされます。どのような例外がスローされるかは、ヘルプやMSDNを調べることにより、ある程度分かります(全て書いてある訳ではありません)。実際にMSDNでFile.OpenText メソッドをご覧になって、どのような例外があるかご確認ください。

以下に、ある特定の種類の例外だけをキャッチする例を示します。FileNotFoundExceptionだけをキャッチするには、次のようにします。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}

CatchでExceptionを指定するとすべての例外をキャッチしますが、このようにキャッチする例外の種類を指定することもできます。

Catchブロックを追加する

FileNotFoundExceptionだけでなく、UnauthorizedAccessExceptionがスローされた時もキャッチできるようにしてみましょう。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As UnauthorizedAccessException
    'UnauthorizedAccessExceptionをキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (UnauthorizedAccessException ex)
{
    //UnauthorizedAccessExceptionをキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}

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

最後にExceptionをキャッチ

上記のコードでは、FileNotFoundExceptionとUnauthorizedAccessExceptionしかキャッチできません。それ以外の例外がスローされたときは、全く例外処理を行わなかった時と同じようになってしまいます。そうならないように、最後にExceptionをキャッチするとよいでしょう。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.FileNotFoundException
    'FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As UnauthorizedAccessException
    'UnauthorizedAccessExceptionをキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As Exception
    'それ以外の例外をキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException ex)
{
    //FileNotFoundExceptionをキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (UnauthorizedAccessException ex)
{
    //UnauthorizedAccessExceptionをキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (Exception ex)
{
    //それ以外の例外をキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}

このようにすると、FileNotFoundExceptionでもUnauthorizedAccessExceptionでもない例外がスローされたときに、最後のCatchブロックが実行されるようになります。

なぜExceptionで全ての例外をキャッチできるか

CatchにExceptionを指定すると全ての例外をキャッチできて、FileNotFoundExceptionを指定するとFileNotFoundExceptionだけをキャッチできると説明しましたが、もう少し詳しく説明しましょう。

Exceptionで全ての例外をキャッチできるのは、全ての例外クラスはExceptionクラスから派生しているからです。FileNotFoundExceptionクラスも、UnauthorizedAccessExceptionクラスも、その他の例外クラスもすべてExceptionクラスから派生しています。

そして、スローされた例外の型がCatchに指定された例外クラスまたはその基本クラスと一致すれば、そのCatchブロックが実行されます。

例えば、CatchにIOExceptionを指定すると、IOExceptionクラスから派生したPathTooLongException、DirectoryNotFoundException、FileNotFoundExceptionなどの例外をすべてキャッチできます。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As System.IO.IOException
    'PathTooLongException、DirectoryNotFoundException、
    'FileNotFoundExceptionなど、IOExceptionから派生した例外を
    'キャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As Exception
    'それ以外の例外をキャッチした時
    Console.WriteLine(ex.Message)
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.IOException ex)
{
    //PathTooLongException、DirectoryNotFoundException、
    //FileNotFoundExceptionなど、IOExceptionから派生した例外を
    //キャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (Exception ex)
{
    //それ以外の例外をキャッチした時
    Console.WriteLine(ex.Message);
    return string.Empty;
}

Catchの順番

複数のCatchブロックがあるときにTryブロックで例外がスローされると、その例外の種類に合致するCatchを上から順番に検索し、見つかった初めのCatchブロックだけが実行されます。

例えば、Exceptionが指定されたCatchブロックを先頭に書いてしまったら、全ての例外がそのCatchブロックで処理されてしまうため、それ以降のCatchブロックは一切実行されなくなります。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch ex As Exception
    '全ての例外をキャッチ
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As System.IO.FileNotFoundException
    '実行されないコード
    Console.WriteLine(ex.Message)
    Return String.Empty
Catch ex As UnauthorizedAccessException
    '実行されないコード
    Console.WriteLine(ex.Message)
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (Exception ex)
{
    //全ての例外をキャッチ
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (System.IO.FileNotFoundException ex)
{
    //実行されないコード
    Console.WriteLine(ex.Message);
    return string.Empty;
}
catch (UnauthorizedAccessException ex)
{
    //実行されないコード
    Console.WriteLine(ex.Message);
    return string.Empty;
}

上記の例は、コンパイルエラー(「前の catch 句はこれ、またはスーパー型 ('System.Exception') の例外のすべてを既にキャッチしました。」)となります。

Exceptionオブジェクトが必要ないとき

CatchブロックでExceptionオブジェクトを取得する必要ないときは、次のように省略できます。

[VB.NET]
Try
    'filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath)
Catch
    '全ての例外をキャッチする
    Return String.Empty
End Try
[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch
{
    //全ての例外をキャッチする
    return string.Empty;
}

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

[C#]
try
{
    //filePathをUTF-8コードとして開く
    sr = System.IO.File.OpenText(filePath);
}
catch (System.IO.FileNotFoundException)
{
    //FileNotFoundExceptionをキャッチする
    Console.WriteLine("ファイルが存在しません。");
    return string.Empty;
}
catch
{
    //FileNotFoundException以外の例外をキャッチする
    return string.Empty;
}

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

今まではFile.OpenTextメソッドのみをTryブロックに入れていましたが、ファイルの読み込みでもエラーは発生しますので、ReadToEndメソッドもTryブロックに入れたほうがよいでしょう。ただしその場合、開いたファイルを確実に閉じるために、全てのCatchブロックでCloseメソッドを呼び出す必要があります。しかし通常はそのようなことはせず、Finallyブロックを使用します。

[VB.NET]
Public Function ReadAllText(ByVal filePath As String) As String
    'ファイルが存在しているか調べる
    If System.IO.File.Exists(filePath) Then
        Return String.Empty
    End If

    Dim sr As System.IO.StreamReader = Nothing
    Dim s As String
    Try
        'filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath)
        '内容をすべて読み込む
        s = sr.ReadToEnd()
    Catch ex As Exception
        '例外をキャッチした時
        Console.WriteLine(ex.Message)
        Return String.Empty
    Finally
        '確実にファイルを閉じる
        If Not (sr Is Nothing) Then
            '閉じる
            sr.Close()
        End If
    End Try

    '結果を返す
    Return s
End Function
[C#]
public string ReadAllText(string filePath)
{
    //ファイルが存在しているか調べる
    if (System.IO.File.Exists(filePath))
    {
        return string.Empty;
    }

    System.IO.StreamReader sr = null;
    string s;
    try
    {
        //filePathをUTF-8コードとして開く
        sr = System.IO.File.OpenText(filePath);
        //内容をすべて読み込む
        s = sr.ReadToEnd();
    }
    catch (Exception ex)
    {
        //例外をキャッチした時
        Console.WriteLine(ex.Message);
        return string.Empty;
    }
    finally
    {
        //確実にファイルを閉じる
        if (sr != null)
        {
            //閉じる
            sr.Close();
        }
    }

    //結果を返す
    return s;
}

Finallyブロックのコードは確実に実行されますので、上記のようにすれば、確実にファイルを閉じることができます。

さらにFinallyについて詳しくは、「Dispose、Closeが確実に呼び出されるようにする」をご覧ください。

例外をキャッチしなかった時

エラーが発生した時にエラー処理を行わなかった時は、通常、「アプリケーションのコンポーネントで、ハンドルされていない例外が発生しました。...」というダイアログが表示されます。このダイアログを表示せずに、適切な処理を行う方法は、「捕捉されなかった例外がスローされたことを知る」で説明しています。

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

ここではエラー処理の基本のみを説明しました。決してこれが全てではありません。

例えば、例外の再スローについては説明しませんでした。VB.NETのCatchステートメントに付けるWhen句や、「On Error GoTo」を使った非構造化例外処理も説明しませんでした。これらについては、リファレンスやMSDNをご覧ください。