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

この記事は[C# Advent Calendar 2017](https://qiita.com/advent-calendar/2017/csharp)の19日目の記事です。

18日目は[@usamik26](https://twitter.com/usamik26)さんの「[Moq : Mocking Framework for .NET - Qiita](https://qiita.com/usamik26/items/42959d8b95397d3a8ffb)」でした。

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

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

[^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/

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

免責事項
===

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

前提条件
===
ここで「エラー」と呼んでいるのは例外(Exception)ではなく、ロジックのエラー(いわゆる業務エラー)のことを指しています。エラーの種類については、[赤間さん](https://twitter.com/nakama00)のエントリ[.NETの例外処理](https://blogs.msdn.microsoft.com/nakama/2008/12/29/net-part-1/)の定義に倣います。

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

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

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

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

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

### メリット

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

### デメリット

- エラー時は実行時例外が発生してしまう可能性が高い


2. 戻り値がデフォルト値かどうかでエラー有無を判定する
---

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

```csharp
// メソッド定義
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つの変数で扱える

### デメリット

- エラーがあったかどうかの検出しかできない
    - どのようなエラーなのかが表現できない


3. 戻り値と同じ型でエラーも扱う
---

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

```csharp
// メソッド定義
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);	
}
```

### メリット

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

### デメリット

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


4. 戻り値でエラー有無、出力引数で処理結果を表す
---

エラーかどうかと処理結果を分けます。成功/失敗のような単純なケースでは有用です。
C# 7.0で追加された[出力変数宣言](http://ufcpp.net/study/csharp/cheatsheet/ap_ver7/#out-var)を使えば、すっきりと書くことができます。

```csharp
// メソッド定義
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);	
}
```

### メリット

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

### デメリット

- エラーがあったかどうかの検出しかできない
    - どのようなエラーなのかが表現できない


5. 戻り値でエラーコード、出力引数で処理結果を表す
---

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

```csharp
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);
}
```

### メリット

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

### デメリット

- エラーがない場合を表す値がシグネチャからは分からない
    - あくまで「取り決め」としてエラーでない値を定める必要がある


6. 列挙型の戻り値でエラーの種類、出力引数で処理結果を表す
---

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

```csharp
// エラーの種類
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;
  }
}
```

### メリット

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

### デメリット

- エラーチェックの種類ごとに列挙型を定義しないといけない
- エラーとするケースが増えても呼び出し側ではそのことがわからない


7. 処理結果とエラーの種類をValueTupleで一緒に返す
---

ひとつ前とほぼ同じですが、C# 7.0で導入された[ValueTuple](http://ufcpp.net/study/csharp/datatype/tuples/)を使って処理結果とエラーを一度に返します。

```csharp
// エラーの種類
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さんから[指摘あり](https://qiita.com/masaru_b_cl/items/104f1df602ac56487806#comment-539c261f82fba5fc7afd))  
  [NuGet Gallery | System.ValueTuple](https://www.nuget.org/packages/System.ValueTuple/)

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

C# 7.0から導入された[型スイッチ](http://ufcpp.net/study/csharp/datatype/typeswitch/)を活用する方法です。エラーを表すのに独自の型を作成するのでなく、既存の例外を再利用します。

※エントリ公開後に[@kazuk](https://twitter.com/kazuk)さんに教えてもらいました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">エラー値としてのenumはexceptionを使うのも割と選択肢として悪くない<br><br>return new indexoutofrangeexception</p>&mdash; Kazuhiko Kikuchi (@kazuk) <a href="https://twitter.com/kazuk/status/942911209774313472?ref_src=twsrc%5Etfw">2017年12月19日</a></blockquote>


```csharp
// 独自定義の例外
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#では使えない


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

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

```csharp
// メソッド定義
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.");
  }
}
```

### メリット

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

### デメリット

- 例外は「重い」処理
- 例外は本当に「例外的」な時だけ使い、フロー制御に使うべきではない
    - [例外設計における大罪 - SlideShare : 乱用(p.12)](https://www.slideshare.net/t_wada/exception-design-by-contract/12)

まとめ
===

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

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

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

20日目は[@4_mio_11](https://twitter.com/4_mio_11)さんの[視て、わかる！C#7.1までの言語機能(スライド)](https://speakerdeck.com/rossam/shi-te-wakaru-c-number-7-dot-1madefalseyan-yu-ji-neng)です。


サンプルコード
===

本エントリのサンプルコードは、みんな大好き[LINQPad](https://www.linqpad.net/)の*.linqファイルとしてGitHubにアップしてあります。

https://github.com/masaru-b-cl/csharp-ac2017