📝

C#でResult型を作ってみた

2023/11/26に公開

前書き

C#を使っているときにいわゆるResult型を書けないかなと思ったので書いてみました.

Code

GitHubで配布してます.
https://github.com/shunsock/dotnet_playground/tree/main/Utils

Usage

  • 以下をimport
    • Utils.ResultType;
    • Utils.ResultType.Data;
    • Utils.ResultType.Fail;
  • static functionを以下のように定義する
    • 簡単!
static ResultData<decimal> Divide(int a, int b)
{
    if (b == 0)
    {
        return Result.Fail<decimal>(ErrorCode.BadRequest);
    }

    return Result.Ok<decimal>((decimal) a / b);
}

Implementation

Directory Architecture

.
├── Program.cs
├── ResultType
│   ├── Data
│   │   └── ResultData.cs
│   ├── Fail
│   │   ├── ErrorCode.cs
│   │   └── ErrorMessage.cs
│   ├── Result.cs
│   └── Status
│       └── ResultStatus.cs
└── Utils.csproj

Result.cs (Static Class)

  • 冒頭の関数で用いたstatic function, Ok, Failを定義している
  • valueはData定義時に型を決める関係上, Failも<T>を用いている
using Utils.ResultType.Data;
using Utils.ResultType.Fail;
using Utils.ResultType.Status;

namespace Utils.ResultType
{
    public static class Result
    {
        public static ResultData<T> Ok<T>(T value)
        {
            return new ResultData<T>(
                status: ResultStatus.Ok,
                value: value
            );
        }

        public static ResultData<T> Fail<T>(ErrorCode errorCode)
        {
            return new ResultData<T>(
                status: ResultStatus.Fail,
                errorCode: errorCode,
                value: default(T)
            );
        }
    }
}

Status

  • 単純なEnum
namespace Utils.ResultType.Status
{
    public enum ResultStatus
    {
        Ok,
        Fail
    }
}

Fail

  • 単純なエラーコード
namespace Utils.ResultType.Fail
{
    public enum ErrorCode
    {
        None,
        BadRequest,
        Unauthorized,
        Forbidden,
        NotFound
    }
}
  • ErrorMessageを表示するためのstatic関数
  • Enumの引数を受け取ってstringを返す
namespace Utils.ResultType.Fail
{
    public static class ErrorMessage
    {
        public static string Show(ErrorCode code)
        {
            return code switch
            {
                ErrorCode.BadRequest => "Failed!: Bad Request",
                ErrorCode.Unauthorized => "Failed!: Unauthorized",
                ErrorCode.Forbidden => "Failed!: Forbidden",
                ErrorCode.NotFound => "Failed!: Not Found",
                ErrorCode.None => throw new InvalidOperationException(message: "ErrorCode is None"),
                _ => throw new ArgumentException(message: "Unknown ErrorCode")
            };
        }
    }
}

Data

  • GetValue関数は以下を念頭に作成
    • Failならデータを入れない
    • Okでdefault, つまり, OK();なら呼び出し元でこの関数を使うことはない
using Utils.ResultType.Fail;
using Utils.ResultType.Status;

namespace Utils.ResultType.Data
{
    public class ResultData<T>
    {
        public readonly ResultStatus Status;
        public readonly ErrorCode ErrorCode;
        private readonly T? _value;

        public ResultData(ResultStatus status, ErrorCode? errorCode = null, T? value = default)
        {
            Status = status;
            ErrorCode = errorCode == null ? ErrorCode.None : errorCode.Value; 
            _value = value;
        }

        public T GetValue()
        {
            if (Status == ResultStatus.Fail)
            {
                throw new InvalidOperationException("No Data with Status == Fail");
            }

            if (_value == null)
            {
                throw new InvalidOperationException(message: "Data is Null");
            }

            return _value!;
        }
    }
}

Discussion