導入
「見つからなかった」「入力が不正」——これらは想定内の失敗です。想定内の失敗まで例外で表すと、呼び出し側は毎回 try-catch を強いられます。Result / Option パターンは、成功か失敗かを戻り値の型で表し、呼び出し側に処理を促します。
図解
flowchart LR
F["Divide(10, 0)"] --> R["Result<int>"]
R -->|IsSuccess| S["Value を使う"]
R -->|!IsSuccess| E["Error を表示"]
style R fill:#e1f5fe
サンプル
Print(Divide(10, 2)); // 成功: 5
Print(Divide(10, 0)); // 失敗: ゼロ除算
void Print(Result<int> r) =>
Console.WriteLine(r.IsSuccess ? $"成功: {r.Value}" : $"失敗: {r.Error}");
Result<int> Divide(int a, int b) =>
b == 0 ? Result<int>.Fail("ゼロ除算") : Result<int>.Ok(a / b);
class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string Error { get; }
private Result(bool ok, T? value, string error) { IsSuccess = ok; Value = value; Error = error; }
public static Result<T> Ok(T value) => new(true, value, "");
public static Result<T> Fail(string error) => new(false, default, error);
}
- 想定内の失敗は例外ではなく戻り値の型で表す
- 呼び出し側は
IsSuccessを見て、成功ならValue、失敗ならErrorを扱う - 「失敗の可能性」が型に現れるので、扱い忘れを防げる(
Optionは「値があるか無いか」版)
やってみよう
Divide の失敗理由を変えたり、負の入力を弾く条件を足したりして、Result に載る Error の内容を確かめましょう。呼び出し側の分岐はそのまま使えます。
演習
class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
private Result(bool ok, T? value) { IsSuccess = ok; Value = value; }
public static Result<T> Ok(T value) => new(true, value);
public static Result<T> Fail() => new(false, default);
}
// TODO: 空文字なら Fail、それ以外は長さを Ok で返す Parse を書いてください
Result<int> Parse(string s) => ___;
var r = Parse("hello");
Console.WriteLine(r.IsSuccess ? r.Value : -1); // 5
- 期待される出力:
5
ヒント1を見る
string.IsNullOrEmpty(s) ? Result<int>.Fail() : Result<int>.Ok(s.Length)
ヒント2を見る
“hello” は長さ5なので Ok(5) が返り、IsSuccess は true になります
まとめ
- Result/Optionは成功・失敗を戻り値の型で表す
- 想定内の失敗を例外にせず、扱い忘れを型で防ぐ
- 呼び出し側は分岐で成功値・失敗理由を扱う
次回: やってはいけない設計=アンチパターンと、パターンの使いどころです。