第五章 错误处理
传统派: 相信后人的智慧
函数式: 承认每一步都会失败
传统异常处理是“出问题 -> 跳出去 -> 另想办法”
函数式错误处理是“每步都承认有可能失败,提前规划好”
传统派 - 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; }
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); } }
|
最佳实践
- 避免使用 null,而是使用 Option
- ROP时,使用委托来进行Log减少副作用
- 阻止try catch(使用SafelyExecute)
- 尽量回退 (Fallback)而不是Fail
千万别
- 混用异常和
Result
- 错误信息不明确
- 直接取 Result 的值而不检查是否成功
- 自定义错误类型细粒度过细
Exercises