第五章 错误处理

传统派: 相信后人的智慧

函数式: 承认每一步都会失败

传统异常处理是“出问题 -> 跳出去 -> 另想办法”

函数式错误处理是“每步都承认有可能失败,提前规划好”

传统派 - try catch

具体移步-> c#错误处理

Result 类型

// 传统派
public Product GetProduct(int id)
{
var product = _productRepository.Get(id);
if (product is null)
{
throw new ProductNotFoundException($"找不到ID为 {id} 的产品。");
}
return product;
}

// Result
public Result<Product, string> GetProduct(int id)
{
var product = _productRepository.Get(id);
if (product is null)
{
return Result<Product, string>.Fail($"找不到ID为 {id} 的产品。");
}
return Result<Product, string>.Success(product);
}

一个通用的Result类型示例

public class Result<T, E>
{
private T _value;
private E _error;
public bool IsSuccess { get; private set; }

private Result(T value, E error, bool isSuccess)
{
_value = value;
_error = error;
IsSuccess = isSuccess;
}

public T Value
{
get
{
if (!IsSuccess)
throw new InvalidOperationException("无法从失败的结果中获取 Value。");
return _value;
}
}

public E Error
{
get
{
if (IsSuccess)
throw new InvalidOperationException("无法从成功的结果中获取 Error。");
return _error;
}
}

public static Result<T, E> Success(T value) => new Result<T, E>(value, default, true);

public static Result<T, E> Fail(E error) => new Result<T, E>(default, error, false);
}
public static class Result
{
public static Result<T, string> Success<T>(T value) => new Result<T, string>(value, default, true);
public static Result<T, string> Fail<T>(string error) => new Result<T, string>(default, error, false);
}

Result 类型从根本上改变了我们看待错误的方式:
错误不再是突如其来的中断,而是被预期并合理处理的结果

面向铁路的编程(Railway-Oriented Programming,ROP)

ROP 是一种错误处理方式

ROP有两条轨道: 成功,失败

程序沿着成功轨道行进,发生错误切换到失败轨道,一路跳过.

ROP解决try catch将错误天女散花的问题

ROP 核心 - Bind

public static Result<Tout, E> Bind<Tin, Tout, E>(
this Result<Tin, E> input,
Func<Tin, Result<Tout, E>> bindFunc)
{
return input.IsSuccess
? bindFunc(input.Value)
: Result<Tout, E>.Fail(input.Error);
}

这样,我们就可以搭建处理铁路

public Result<bool, string> HandleData(string input)
{
return ParseInput(input)
.Bind(parsedData => ValidateData(parsedData))
.Bind(validData => TransformData(validData))
.Bind(transformedData => StoreData(transformedData));
}

Bind 拓展

异步支持

public static async Task<Result<TOut, E>> BindAsync<TIn, TOut, E>(
this Result<TIn, E> input,
Func<TIn, Task<Result<TOut, E>>> bindFuncAsync)
{
return input.IsSuccess
? await bindFuncAsync(input.Value)
: Result<TOut, E>.Fail(input.Error);
}

错误映射

我觉得不需要🙅

public static Result<TOut, EOut> Bind<TIn, TOut, EIn, EOut>(
this Result<TIn, EIn> input,
Func<TIn, Result<TOut, EOut>> bindFunc,
Func<EIn, EOut> errorMap)
{
return input.IsSuccess
? bindFunc(input.Value)
: Result<TOut, EOut>.Fail(errorMap(input.Error));
}

Tips

可以通过包装传统派代码,来支持ROP.

public static Result<T, Exception> TryExecute<T>(Func<T> action)
{
try
{
return Result.Success(action());
}
catch (Exception ex)
{
return Result.Fail<T, Exception>(ex);
}
}
public static Result<T, E> SafelyExecute<T, E>(Func<T> function, E error)
{
try
{
return Result.Success(function());
}
catch
{
return Result.Fail<T, E>(error);
}
}

最佳实践

  1. 避免使用 null,而是使用 Option
  2. ROP时,使用委托来进行Log减少副作用
  3. 阻止try catch(使用SafelyExecute)
  4. 尽量回退 (Fallback)而不是Fail

千万别

  1. 混用异常和Result
  2. 错误信息不明确
  3. 直接取 Result 的值而不检查是否成功
  4. 自定义错误类型细粒度过细

Exercises