構文:条件を指定して例外をキャッチするには?[C# 6/VB].NET TIPS

C# 6/VBでは例外処理時に、catch句にwhen句を付加して、例外をキャッチする条件を指定する方法を解説。また、その応用例も紹介する。

» 2016年08月03日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

対象:C# 6.0(Visual Studio 2015)以降、VB 7.0(Visual Studio .NET 2002)以降


 C#でプログラミングしていて、例外をキャッチするときに条件を付けたいと思ったことはないだろうか? 例えば、渡した引数が原因となって例外を引き起こしたときに、特定の引数の場合だけ処理を変えたいといった場合だ。これまでのC#では、取りあえずキャッチしてから、catchブロックの中で例外を検査して処理を分岐させるしかなかった。

 ところでVisual Basic .NET(以降、VB)には、当初から条件付きで例外をキャッチする機能があった。その機能が、ついにC# 6.0にも導入されたのである。本稿では、その使い方を解説しよう。

catch句にwhen句を付ける

 特定の条件のときだけcatchブロックの中身を実行させるには、catch句の後ろにwhen句を付ける。その例を次のコードに示す。

static void CatchSample(string num1, string num2)
{
  try
  {
    // ここで引数num1/num2を使う処理を行う
    // 処理できないときはArgumentExceptionが発生するものとする
  }
  catch (ArgumentException ex) when (ex.ParamName == "num1")
  {
    // ここは、引数num1が例外を引き起こしたときだけ実行される
    Console.WriteLine($"■引数num1(値は{num1})によって例外が発生");
  }
  catch (ArgumentException ex)
  {
    // ここは、num1以外の引数(=引数num2)が例外を引き起こしたときだけ実行される
    Console.WriteLine($"■num1以外の引数(値は{num2})によって例外が発生");
  }
}

Sub CatchSample(num1 As String, num2 As String)
  Try
    ' ここで引数num1/num2を使う処理を行う
    ' 処理できないときはArgumentExceptionが発生するものとする

  Catch ex As ArgumentException When ex.ParamName = "num1"
    ' ここは、引数num1が例外を引き起こしたときだけ実行される
    Console.WriteLine($"■引数num1(値は{num1})によって例外が発生")

  Catch ex As ArgumentException
    ' ここは、num1以外の引数(=引数num2)が例外を引き起こしたときだけ実行される
    Console.WriteLine($"■num1以外の引数(値は{num2})によって例外が発生")

  End Try
End Sub

条件を付けて例外をキャッチするコード例(上:C#、下:VB)
when句の書き方はC#とVBでほぼ同じだ。C#のwhen句では、条件式をかっこでくくらなければならない。

 上の例では、ArgumentExceptionをキャッチするcatchブロックが2つある。最初のcatchブロックにはwhen句が付いており、その条件を満たすときだけ最初のcatchブロックの中身が実行される(このときは、2番目のcatchブロックの中身は実行されない)。

 ArgumentExceptionが発生しても、最初のcatchブロックのwhen句の条件を満たさないときは、2番目のcatchブロックの中身が実行される(このときは、最初のcatchブロックの中身は実行されない)。

応用:常にfalseを返すwhen句

 when句の条件式が常にfalseを返す場合、そのcatchブロックの中身は実行されず、その下のcatchブロックへと処理が流れていく。ちょっとトリッキーではあるが、それを応用して全ての例外をロギングすることもできる。

 発生した全ての例外をログに書き出そうとしたら、どうすればよいだろうか? 全てのcatchブロックにロギングするコードを書くか、あるいは、全ての例外をキャッチしておいて、ロギングしてから例外を検査して処理を分岐させるかだろう。

 ところが、when句を応用すると、そのような処理が簡潔に書ける。まず、常にfalseを返すロギング用のメソッドを作っておく。そして、try句の直後に全ての例外をトラップするcatch句を置き、そこにwhen句を付けて、そのロギング用のメソッドを与えるのだ。こうすると、ロギング用のメソッドは実行されるが、そのcatch句の中身は実行されず、その後に続くcatch句に処理が移っていくのである。

 具体的なサンプルコードを次に示そう。コンソールアプリである(先に示したサンプルコードの具体例にもなっている)。実際に出力された結果を、コメントとして「⇒」記号に続けて掲載してある。ちょっと長いコードだが、どのようにして出力された結果なのかを、じっくり読み解いてみてほしい。

using System;
using static System.Console;

class Program
{
  // 例外が発生するメソッド
  static int SampleMethod(string num1, string num2)
  {
    int n1, n2;
    if (!int.TryParse(num1, out n1))
      throw new ArgumentException("整数に変換できない文字列", nameof(num1));
    if (!int.TryParse(num2, out n2))
      throw new ArgumentException("整数に変換できない文字列", nameof(num2));
    checked
    {
      return n1 * n2;
    }
  }

  // ログに書き出すメソッド(ここではサンプルとしてコンソールに出力)
  static bool DummyLog(Exception ex)
  {
    WriteLine($"DummyLog: {ex.Message}");
    return false; // 常にfalseを返す
  }

  // when句を使う例
  static void CatchSample(string num1, string num2)
  {
    try
    {
      var result = SampleMethod(num1, num2);
      WriteLine($"{num1} * {num2} = {result}");
    }
    catch (Exception ex) when (DummyLog(ex))
    {
      // DummyLogメソッドは常にfalseを返すので、
      // このcatchブロック内は実行されない
    }
    catch (ArgumentException ex) when (ex.ParamName == "num1")
    {
      WriteLine($"■引数num1(値は{num1})によって例外が発生");
    }
    catch (ArgumentException ex)
    {
      WriteLine($"■num1以外の引数(値は{num2})によって例外が発生");
    }
    catch (Exception ex)
    {
      WriteLine($"■例外が発生:{ex.Message}");
    }
  }

  static void Main(string[] args)
  {
    CatchSample("2", "3");
    // ⇒ 2 * 3 = 6

    CatchSample("a", "3");
    // ⇒ DummyLog: 整数に変換できない文字列
    // パラメーター名:num1
    // ■引数num1(値はa)によって例外が発生

    CatchSample("2", "BB");
    // ⇒ DummyLog: 整数に変換できない文字列
    // パラメーター名:num2
    // ■num1以外の引数(値はBB)によって例外が発生

    CatchSample("123456789", "123");
    // ⇒ DummyLog: 算術演算の結果オーバーフローが発生しました。
    // ■例外が発生:算術演算の結果オーバーフローが発生しました。

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1

  ' 例外が発生するメソッド
  Function SampleMethod(num1 As String, num2 As String) As Integer
    Dim n1, n2 As Integer
    If (Not Integer.TryParse(num1, n1)) Then
      Throw New ArgumentException("整数に変換できない文字列", NameOf(num1))
    End If
    If (Not Integer.TryParse(num2, n2)) Then
      Throw New ArgumentException("整数に変換できない文字列", NameOf(num2))
    End If
    Return n1 * n2
  End Function

  ' ログに書き出すメソッド(ここではサンプルとしてコンソールに出力)
  Function DummyLog(ex As Exception) As Boolean
    WriteLine($"DummyLog: {ex.Message}")
    Return False  ' 常にFalseを返す
  End Function

  ' when句を使う例
  Sub CatchSample(num1 As String, num2 As String)
    Try
      Dim result = SampleMethod(num1, num2)
      WriteLine($"{num1} * {num2} = {result}")

    Catch ex As Exception When DummyLog(ex)
      ' DummyLogメソッドは常にFalseを返すので、
      ' このcatchブロック内は実行されない

    Catch ex As ArgumentException When ex.ParamName = "num1"
      WriteLine($"■引数num1(値は{num1})によって例外が発生")

    Catch ex As ArgumentException
      WriteLine($"■num1以外の引数(値は{num2})によって例外が発生")

    Catch ex As Exception
      WriteLine($"■例外が発生:{ex.Message}")

    End Try
  End Sub

  Sub Main()
    CatchSample("2", "3")
    ' ⇒ 2 * 3 = 6

    CatchSample("a", "3")
    ' ⇒ DummyLog: 整数に変換できない文字列
    ' パラメーター名: num1
    ' ■引数num1(値はa)によって例外が発生

    CatchSample("2", "BB")
    ' ⇒ DummyLog: 整数に変換できない文字列
    ' パラメーター名: num2
    ' ■num1以外の引数(値はBB)によって例外が発生

    CatchSample("123456789", "123")
    ' ⇒ DummyLog: 算術演算の結果オーバーフローが発生しました。
    ' ■例外が発生:算術演算の結果オーバーフローが発生しました。

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

when句を応用して全ての例外をロギングするコード例(上:C#、下:VB)
絶対に実行されないcatch句というのはちょっとトリッキーではあるが、ご覧のように「全ての例外をロギングする」という目的を簡潔に記述できる。
なお、「SampleMethod」メソッドで使っているnameof演算子については「.NET TIPS:構文:文字列にクラス名などを間違えないようにコーディングするには?[C# 6.0]」をご覧いただきたい。
「DummyLog」メソッド/「CatchSample」メソッドに出てくるWriteLineメソッドの呼び出し方と、「Main」メソッド末尾のReadKeyメソッドの呼び出し方については、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」、または、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」をご覧いただきたい。
「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。
WriteLineメソッドに引数として渡している「$」記号付きの文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」をご覧いただきたい。

まとめ

 C# 6.0からは、VB同様に、catch句に条件を指定できるようになった。ちょっとトリッキーではあるが、それを応用して全ての例外をロギングする方法も紹介した。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。