94
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#のエラーハンドリング実装あれこれ

Last updated at Posted at 2017-12-18

この記事はC# Advent Calendar 2017の19日目の記事です。

18日目は@usamik26さんの「Moq : Mocking Framework for .NET - Qiita」でした。

C#におけるエラーハンドリング

C#は2002年に.NET Frameworkとともにバージョン1.0がリリースされ1、既に15年ほどの歴史があり、2017年12月19日現在はバージョン7.2まで進化しています2。その歴史の中では、当然言語機能も増え、エラーハンドリングのやり方も徐々に種類が増えてきました。

そこで、C#に置けるエラーハンドリングの実装を一度ここで整理し、特徴をまとめてみます。

免責事項

このリストはもちろん完全ではありません。誤字脱字から認識の齟齬、他にもこんなやりかたがあるよ、特徴にこんなものがあるよ、などありましたら、遠慮なく編集リクエストコメントにてお知らせください。

前提条件

ここで「エラー」と呼んでいるのは例外(Exception)ではなく、ロジックのエラー(いわゆる業務エラー)のことを指しています。エラーの種類については、赤間さんのエントリ.NETの例外処理の定義に倣います。

エラーハンドリング実装あれこれ

  1. エラーハンドリングをしない

業務エラーと判定する処理がない場合、エラーハンドリングを端折ることもあるでしょう。

// メソッド定義
string NonErrorHandling(string name){
  return $"Hello! {name}";
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var result = NonErrorHandling(userName);	
}

メリット

  • エラー処理がない分メインの処理はわかりやすくなる

デメリット

  • エラー時は実行時例外が発生してしまう可能性が高い
  1. 戻り値がデフォルト値かどうかでエラー有無を判定する

処理結果を返す際、エラーがあった場合は処理結果の型の既定値(例:intなら0、参照型ならnull)を返します。ただ、本Advent Calendar 9日目の猪股さんによる「Null 非許容な参照型 - Qiitaにある通り、今後は参照型でもnullを排除していく方向ですので、注意が必要です。

// メソッド定義
string ReturningResultOnly(string name)
{
  if (string.IsNullOrEmpty(name)) return null;
	
  return $"Hello! {name}";
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var result = ReturningResultOnly(userName);
  if (result == null)
  {
    Console.WriteLine("Some Error!");
    return;
  }
  
  Console.WriteLine(result);
}

メリット

  • 結果とエラーを1つの変数で扱える

デメリット

  • エラーがあったかどうかの検出しかできない
    • どのようなエラーなのかが表現できない
  1. 戻り値と同じ型でエラーも扱う

ひとつ前と戻り値と同じ型でエラーを表すのは同じですが、既定値ではなくあらかじめ決めて置いた値をエラーとして扱います。

// メソッド定義
string ReturningResultOrErrorCode(string name)
{
  if (string.IsNullOrEmpty(name)) return "E001";
	
  // some validation with return error code "Exxx";
	
  return $"Hello! {name}";
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var result = ReturningResultOrErrorCode(userName);
  switch(result)
  {
    case "E001" :
      Console.WriteLine("name is empty!");
      return;
  	
    // case "Exx" :
    //   error handling
    //   return;
  }
  
  Console.WriteLine(result);	
}

メリット

  • 多くの種類のエラーを表現できる
  • エラーコードなどを返し、呼び出し元でエラーコードをもとにエラーメッセージを引くといったことができる

デメリット

  • 処理結果とエラー内容が混ざってしまう
  • 正常値とエラー値の境界がコード上に現れない
    • エラーとする値の範囲をもれなく扱うのが困難
    • 定数として定義しても管理が大変
  • 想定していないエラー値が入り込む余地がある
  1. 戻り値でエラー有無、出力引数で処理結果を表す

エラーかどうかと処理結果を分けます。成功/失敗のような単純なケースでは有用です。
C# 7.0で追加された出力変数宣言を使えば、すっきりと書くことができます。

// メソッド定義
bool ReturningIsValidAndSettingResultToOutVar(string name, out string greeting)
{
  if (string.IsNullOrEmpty(name))
  {
    greeting = null;
    return false;
  }

  greeting = $"Hello! {name}";
  
  return true;
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var isValid = ReturningIsValidAndSettingResultToOutVar(userName, out var result);
  if (!isValid)
  {
    Console.WriteLine("Some Error!");
    return;
  }
  
  Console.WriteLine(result);	
}

メリット

  • 結果とエラーが混ざらない

デメリット

  • エラーがあったかどうかの検出しかできない
    • どのようなエラーなのかが表現できない
  1. 戻り値でエラーコード、出力引数で処理結果を表す

ひとつ前と異なり、エラー有無をエラーコードで返します。

string ReturningErrorCodeAndSettingResultToOutVar(string name, out string greeting)
{
  if (string.IsNullOrEmpty(name))
  {
    greeting = null;
    return "E001";
  }	

  // some validation with return error code "Exxx";

  greeting = $"Hello! {name}";

  return string.Empty;
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var errorCode = ReturningErrorCodeAndSettingResultToOutVar(userName, out var result);
  switch(errorCode)
  {
    case "E001" :
      Console.WriteLine("name is empty!");
      return;
  	
    // case "Exx" :
    //   error handling
    //   return;
  }
  
  Console.WriteLine(result);
}

メリット

  • 結果とエラーが混ざらない
  • 多様なエラーを表せる
  • 呼び出し元でエラーコードをもとにエラーメッセージを引くといったことができる

デメリット

  • エラーがない場合を表す値がシグネチャからは分からない
    • あくまで「取り決め」としてエラーでない値を定める必要がある
  1. 列挙型の戻り値でエラーの種類、出力引数で処理結果を表す

現在のところ、最も現実的な実装方法の一つです。

// エラーの種類
enum ErrorType
{
  NoError,
  NameIsEmpty,
  // SomeError
}

// メソッド定義
ErrorType ReturningErrorTypeAndSettingResultToOutVar(string name, out string greeting)
{
  if (string.IsNullOrEmpty(name))
  {
    greeting = null;
    return ErrorType.NameIsEmpty;
  }	

  // some validation with return error code "Exxx";

  greeting = $"Hello! {name}";

  return ErrorType.NoError;
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var errorType = ReturningErrorTypeAndSettingResultToOutVar(userName, out var result);
  switch(errorType)
  {
    case ErrorType.NoError :
      Console.WriteLine(result);
      return;
  
    case ErrorType.NameIsEmpty :
      Console.WriteLine("name is empty!");
      return;
  	
    // case ErrorType.SomeError :
    //   error handling
    //   return;
  }
}

メリット

  • 結果とエラーが混ざらない
  • 多様なエラーを表せる
  • エラーの有無を型で表せる

デメリット

  • エラーチェックの種類ごとに列挙型を定義しないといけない
  • エラーとするケースが増えても呼び出し側ではそのことがわからない
  1. 処理結果とエラーの種類をValueTupleで一緒に返す

ひとつ前とほぼ同じですが、C# 7.0で導入されたValueTupleを使って処理結果とエラーを一度に返します。

// エラーの種類
enum ErrorType
{
  NoError,
  NameIsEmpty,
  // SomeError
}

// メソッド定義
(string result, ErrorType errorType) ReturningResultAndErrorAsValueTuple(string name)
{
  if (string.IsNullOrEmpty(name))
  {
    return (null, ErrorType.NameIsEmpty);
  }	

  // some validation with return error code "Exxx";

  return ($"Hello! {name}", ErrorType.NoError);
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var (result, errorType) = ReturningResultAndErrorAsValueTuple(userName);
  switch(errorType)
  {
    case ErrorType.NoError :
      Console.WriteLine(result);
      return;
  
    case ErrorType.NameIsEmpty :
      Console.WriteLine("name is empty!");
      return;
  	
    // case ErrorType.SomeError :
    //   error handling
    //   return;
  }
}

メリット

  • 結果とエラーが混ざらない
  • 多様なエラーを表せる
  • エラーの有無を型で表せる
  • ValueTupeの分解を使い結果とエラー種類の両方の変数を一度に宣言できる
    • Go言語っぽい書き味になる

デメリット

  • エラーチェックの種類ごとに列挙型を定義しないといけない
  • エラーとするケースが増えても呼び出し側ではそのことがわからない
  • ValueTupeがない古いC#では使えない.NET FrameworkではNuGetからパッケージを導入しないといけない(コメント欄にて@aetosさんから指摘あり)
    NuGet Gallery | System.ValueTuple

8.例外を戻して型スイッチで分岐

C# 7.0から導入された型スイッチを活用する方法です。エラーを表すのに独自の型を作成するのでなく、既存の例外を再利用します。

※エントリ公開後に@kazukさんに教えてもらいました。

// 独自定義の例外
class MyException : Exception
{
  public string ErrorCode {get; set;}
}

// メソッド定義
(string result, Exception exception) ReturningResultAndExceptionAsValueTuple(string name)
{
  if (string.IsNullOrEmpty(name))
  {
    return (null, new ArgumentNullException(nameof(name)));
  }

  string person = FindByName(name);
  if (person == null)
  {
    return (null, new MyException { ErrorCode = "E001" });
  }

  return ($"Hello! {name}", null);
}

string FindByName(string name)
{
  // 名前から人を探しに行く
  if (name.StartsWith("0"))
  {
    // 見つからなかった
    return null;
  }

  return "FullName";
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  var (result, exception) = ReturningResultAndExceptionAsValueTuple(userName);
  switch(exception)
  {
    case ArgumentNullException ex :
      Console.WriteLine($"{ex.ParamName} is empty!");
      return;

    case MyException ex :
      Console.WriteLine($"Some Error: {ex.ErrorCode}");
      return;

    case Exception ex:
		  throw ex;
  }
  Console.WriteLine(result);
}

メリット

  • 既存の例外が標準ライブラリなどに豊富にあるため流用可能
  • 独自の業務エラー用例外を作れば付加情報も扱える
  • 業務例外以外はそのままthrowすることもできる
  • エラーなしを表す値を定義しなくてよい(nullでいける)

デメリット

  • 列挙型の時と同様にエラーとするケースが増えても呼び出し側ではそのことがわからない
  • null非許容の参照型が導入されたら注意が必要
  • 型スイッチがない古いC#では使えない

番外. 例外を使って業務エラーも処理する

業務エラーも例外を使って表します。いわゆる「部品」における引数の検証などではよく使われます。

// メソッド定義
string ReturningResultAndThrowingBusinessException(string name)
{
  if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
	
  return $"Hello! {name}";
}

void Main()
{
  // 呼び出し
  var userName = "Sho";
  try
  {
    var result = ReturningResultAndThrowingBusinessException(userName);
    Console.WriteLine(result);
  }
  catch (ArgumentNullException ex)
  {
    Console.WriteLine($"argument `{ex.ParamName}` is empty.");
  }
}

メリット

  • エラー時に即時に終了することが分かりやすい

デメリット

まとめ

現時点ではいずれの実装でも完全とは言えません。個人的には「小ネタ チェック例外とUnion型 | ++C++; // 未確認飛行 C ブログ」でも言及されているような、Union型があればいいなーとは思いますが、今のところ導入されそうな様子はありません。

しかし現実は待ってくれません。与えられた材料や条件を元に、それぞれのメリット、デメリットを天秤に掛け、最適と思えるやり方を選ぶことになります。

本エントリーが少しでもその助けとなれば幸いです。

20日目は@4_mio_11さんの視て、わかる!C#7.1までの言語機能(スライド)です。

サンプルコード

本エントリのサンプルコードは、みんな大好きLINQPadの*.linqファイルとしてGitHubにアップしてあります。

  1. https://en.wikipedia.org/wiki/C_Sharp_(programming_language)#Versions

  2. VS 15.5で正式サポート開始 https://blogs.msdn.microsoft.com/visualstudio/2017/12/04/visual-studio-2017-version-15-5-visual-studio-for-mac-released/

94
93
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
94
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?