找回密码
 立即注册
首页 业界区 业界 告别 throw exception!为什么 Result<T> 才是业务逻辑 ...

告别 throw exception!为什么 Result<T> 才是业务逻辑的正确选择

均浇 2026-1-20 20:55:00
目录

  • 引言:一个普遍存在的“坏味道”
  • 一、异常的“原罪” —— 我们一直在滥用它

    • 1.1 异常的本质是什么?
    • 1.2 业务逻辑 ≠ 异常情况

  • 二、Result——业务逻辑的"优雅降级"

    • 2.1 什么是Result?
    • 2.2 如何正确使用Result?

  • 三、性能对决 —— 几近碾压的性能差距

    • 3.1 部分测试代码
    • 3.2 测试结果:触目惊心
    • 3.3 并发场景下:性能差距依旧不忍直视

  • 四、为什么异常在业务场景下如此"昂贵"?

    • 4.1 CLR异常机制的底层原理

      • 4.1.1 异常对象的构造过程
      • 4.1.2 堆栈跟踪的真实代价
      • 4.1.3 JIT和AOT编译对异常的影响

    • 4.2 异常处理的内存分配细节

      • 4.2.1 异常对象的内存布局
      • 4.2.2 GC的影响

    • 4.3 CPU级别的性能影响

      • 4.3.1 现代CPU的异常处理开销
      • 4.3.2 对比正常返回和异常返回

    • 4.4 对比其他编程语言

      • 4.4.1 Java的异常机制
      • 4.4.2 Go的错误处理哲学
      • 4.4.3 Rust的Result类型
      • 4.4.4 C++的错误处理

    • 4.5 .NET Core的改进和局限

  • 五、Result的进阶优势

    • 5.1 丰富的错误信息
    • 5.2 函数式编程支持
    • 5.3 更好的API设计

  • 六、 Result模式的一些弊端

    • 6.1 "if地狱"问题(条件判断泛滥)
    • 6.2 值类型(struct)vs 引用类型(class)的两难选择
    • 6.3 异步编程的复杂性
    • 6.4 类型系统冗长
    • 6.5 其他问题

  • 七、常见问题与答疑
  • 八、总结

引言:一个普遍存在的“坏味道”

如果你在C#项目中看到这样的代码,一定不会感到陌生:
  1. public User Login(string username, string password){    var user = FindUser(username);    if (user == null)        throw new Exception("用户不存在");  // ❌ 熟悉的模式        if (!VerifyPassword(user, password))        throw new Exception("密码错误");    // ❌ 另一个熟悉的模式        return user;}
复制代码
这种使用异常来处理业务逻辑的做法,几乎成了C#开发的“标准范式”。
可是,从来如此,便是对的么?
一、异常的“原罪” —— 我们一直在滥用它

1.1 异常的本质是什么?

首先,我们要明白C#语言里的异常(Exception)的设计初衷:
  1. // 这些才是异常真正的使用场景:public void ReadFile(string path){    if (string.IsNullOrEmpty(path))        throw new ArgumentNullException(nameof(path));  // ✅ 参数检查        if (!File.Exists(path))        throw new FileNotFoundException($"文件不存在: {path}");  // ✅ 系统错误        // 尝试读取文件,可能抛出IOException等    var content = File.ReadAllText(path);}
复制代码
异常是为真正的"异常情况"设计的,比如:

  • 系统资源不可用(文件不存在、数据库连接失败)
  • 程序状态异常(空指针、数组越界)
  • 参数验证失败(前置条件不满足)
1.2 业务逻辑 ≠ 异常情况

业务错误(用户不存在、密码错误、余额不足)是可预见的正常业务流程,而不是异常情况。
把业务错误用异常处理,就像:

  • 用"地震警报"来处理"家里没米了"
  • 用"消防车"来运送"快递包裹"
  • 用"手术室"来处理"感冒发烧"
这是对异常机制的严重滥用!
二、Result——业务逻辑的"优雅降级"

2.1 什么是Result?

一个简单,具备基本功能的Result类如下:
  1. public class Result{    public bool Success { get; }    public T? Value { get; }    public string? Error { get; }        private Result(T value) { Success = true; Value = value; Error = null; }    private Result(string error) { Success = false; Value = default; Error = error; }        public static Result Ok(T value) => new(value);    public static Result Fail(string error) => new(error);}
复制代码
2.2 如何正确使用Result?
  1. public Result Login(string username, string password){    if (string.IsNullOrEmpty(username))        return Result.Fail("用户名不能为空");  // ✅ 明确返回业务错误        if (string.IsNullOrEmpty(password))        return Result.Fail("密码不能为空");    // ✅ 明确返回业务错误        var user = FindUser(username);    if (user == null)        return Result.Fail("用户不存在");      // ✅ 明确返回业务错误        if (!VerifyPassword(user, password))        return Result.Fail("密码错误");        // ✅ 明确返回业务错误        return Result.Ok(user);                   // ✅ 明确返回成功}
复制代码
三、性能对决 —— 几近碾压的性能差距

3.1 部分测试代码

项目环境:
  1.                         Exe                net8.0                enable                enable                latest       
复制代码
部分测试代码:
  1. public class LoginService {     private readonly Dictionary _users = new()     {         ["valid_user"] = new User { Id = 1, Username = "valid_user" }     };         public User LoginWithException(string username, string password)     {         if (!_users.TryGetValue(username, out var user))             throw new BusinessException("用户不存在");         if (password != "correct_password")             throw new BusinessException("密码错误");         return user;     }         public Result LoginWithResult(string username, string password)     {         if (!_users.TryGetValue(username, out var user))             return Result.Fail("用户不存在");         if (password != "correct_password")             return Result.Fail("密码错误");         return Result.Ok(user);     } }
复制代码
  1. public class PerformanceTester{    private readonly LoginService _service = new();    private readonly Random _random = new(42);    public void RunAllTests(int iterations = 1000000)    {        Console.WriteLine($"性能对比测试 - 迭代次数: {iterations:N0}");        Console.WriteLine("=".PadRight(60, '='));        // 测试1:成功路径(正常情况)        TestSuccessPath(iterations);        // 测试2:失败路径(错误情况)        TestErrorPath(iterations);        // 测试3:混合路径(30%成功率)        TestMixedPath(iterations, 0.3);    }    private void TestSuccessPath(int iterations)    {        Console.WriteLine("\n测试1:成功路径(100%成功)");        // 异常方式        var exceptionTime = Measure(() =>        {            try            {                _service.LoginWithException("valid_user", "correct_password");            }            catch            {                // 不应该发生            }        }, iterations, "异常方式");        // Result方式        var resultTime = Measure(() =>        {            var result = _service.LoginWithResult("valid_user", "correct_password");            if (!result.Success)            {                // 不应该发生            }        }, iterations, "Result方式");        PrintComparison(exceptionTime, resultTime);    }    private void TestErrorPath(int iterations)    {        Console.WriteLine("\n测试2:失败路径(100%失败)");        // 异常方式        var exceptionTime = Measure(() =>        {            try            {                _service.LoginWithException("invalid_user", "wrong_password");            }            catch (BusinessException)            {                // 预期异常            }        }, iterations, "异常方式");        // Result方式        var resultTime = Measure(() =>        {            var result = _service.LoginWithResult("invalid_user", "wrong_password");            if (result.Success)            {                // 不应该发生            }        }, iterations, "Result方式");        PrintComparison(exceptionTime, resultTime);    }    private void TestMixedPath(int iterations, double successRate)    {        Console.WriteLine($"\n测试3:混合路径({successRate:P0}成功率)");        // 准备测试数据        var testData = new (string user, string pwd, bool shouldSucceed)[iterations];        for (int i = 0; i < iterations; i++)        {            testData[i] = _random.NextDouble() < successRate                ? ("valid_user", "correct_password", true)   // 成功                : ("invalid_user", "wrong_password", false); // 失败        }        // 异常方式        var exceptionTime = MeasureMixed(testData, true, "异常方式");        // Result方式        var resultTime = MeasureMixed(testData, false, "Result方式");        PrintComparison(exceptionTime, resultTime);    }    private static long Measure(Action action, int iterations, string testName)    {        // 预热        for (int i = 0; i < 1000; i++) action();        GC.Collect();        GC.WaitForPendingFinalizers();        GC.Collect();        var sw = Stopwatch.StartNew();        for (int i = 0; i < iterations; i++)        {            action();        }        sw.Stop();        var opsPerSecond = iterations / (sw.ElapsedMilliseconds / 1000.0);        Console.WriteLine($"  {testName}: {sw.ElapsedMilliseconds,8}ms ({opsPerSecond,12:N0} ops/s)");        return sw.ElapsedMilliseconds;    }    private long MeasureMixed(        (string user, string pwd, bool shouldSucceed)[] testData,        bool useException,        string testName)    {        // 预热        for (int i = 0; i < Math.Min(1000, testData.Length); i++)        {            var (user, pwd, _) = testData[i];            if (useException)            {                try                {                    _service.LoginWithException(user, pwd);                }                catch { }            }            else            {                _service.LoginWithResult(user, pwd);            }        }        GC.Collect();        GC.WaitForPendingFinalizers();        GC.Collect();        var sw = Stopwatch.StartNew();        if (useException)        {            for (int i = 0; i < testData.Length; i++)            {                var (user, pwd, _) = testData[i];                try                {                    _service.LoginWithException(user, pwd);                }                catch { }            }        }        else        {            for (int i = 0; i < testData.Length; i++)            {                var (user, pwd, _) = testData[i];                _service.LoginWithResult(user, pwd);            }        }        sw.Stop();        var opsPerSecond = testData.Length / (sw.ElapsedMilliseconds / 1000.0);        Console.WriteLine($"  {testName}: {sw.ElapsedMilliseconds,8}ms ({opsPerSecond,12:N0} ops/s)");        return sw.ElapsedMilliseconds;    }    private static void PrintComparison(long exceptionTime, long resultTime)    {        var speedup = exceptionTime / (double)resultTime;        var improvement = (exceptionTime - resultTime) * 100.0 / exceptionTime;        if (speedup > 1)        {            Console.WriteLine($"Result比Exception快 {speedup:F1}x,性能提升 {improvement:F1}%");        }        else        {            Console.WriteLine($"差异不大: {speedup:F2}x");        }    }}
复制代码
3.2 测试结果:触目惊心

先上图,看测试结果(基于RELEASE模式编译):

3.3 并发场景下:性能差距依旧不忍直视

并发测试核心代码:
  1. /// /// // 并发测试结果类/// public class ConcurrentTestResult{    public int Concurrency { get; set; }    public long ExceptionTime { get; set; }    public long ResultTime { get; set; }    public double ExceptionOpsPerSecond { get; set; }    public double ResultOpsPerSecond { get; set; }}/// /// 并发测试器/// public class ConcurrentPerformanceTester{    private readonly LoginService _service = new();    private readonly Random _random = new(42);    private readonly double _errorRate = 0.3;    public async Task RunConcurrentTests(int totalIterations = 1000000)    {        Console.WriteLine("\n并发性能测试 - 总迭代次数: {0:N0} - 错误率:{1:P0}", totalIterations, _errorRate);        Console.WriteLine("=".PadRight(60, '='));        var concurrencyLevels = new[] { 4, 8, 16, 32, 64, 128, 256 };        foreach (var concurrency in concurrencyLevels)        {            Console.WriteLine($"\n并发数: {concurrency}");            // 预热            await Warmup(concurrency);            // 异常方式并发测试(30%错误率模拟真实场景)            var exceptionTime = await RunConcurrentExceptionTest(                concurrency,                totalIterations,                errorRate: _errorRate            );            // Result方式并发测试            var resultTime = await RunConcurrentResultTest(                concurrency,                totalIterations,                errorRate: _errorRate            );            var exceptionOps = totalIterations / (exceptionTime / 1000.0);            var resultOps = totalIterations / (resultTime / 1000.0);            var speedup = exceptionTime / (double)resultTime;            Console.WriteLine($"  异常: {exceptionTime,5}ms ({exceptionOps,8:N0} ops/s)");            Console.WriteLine($"  Result: {resultTime,5}ms ({resultOps,8:N0} ops/s)");            Console.WriteLine($"  Result快 {speedup:F1}x");        }    }    ///     /// 并发异常测试    ///     ///     ///     ///     ///     private async Task RunConcurrentExceptionTest(        int concurrency,        int totalIterations,        double errorRate)    {        var iterationsPerTask = totalIterations / concurrency;        var tasks = new Task[concurrency];        var sw = Stopwatch.StartNew();        for (int i = 0; i < concurrency; i++)        {            // 每个任务使用自己的随机实例,避免竞争            var taskRandom = new Random(_random.Next());            tasks[i] = Task.Run(() =>            {                for (int j = 0; j < iterationsPerTask; j++)                {                    // 为每次迭代生成测试数据,避免索引问题                    var (user, pwd) = GenerateTestDataForIteration(taskRandom, errorRate);                    try                    {                        _service.LoginWithException(user, pwd);                    }                    catch (BusinessException)                    {                        // 预期异常                    }                }            });        }        await Task.WhenAll(tasks);        sw.Stop();        return sw.ElapsedMilliseconds;    }    ///     /// 并发Result测试    ///     ///     ///     ///     ///     private async Task RunConcurrentResultTest(        int concurrency,        int totalIterations,        double errorRate)    {        var iterationsPerTask = totalIterations / concurrency;        var tasks = new Task[concurrency];        var sw = Stopwatch.StartNew();        for (int i = 0; i < concurrency; i++)        {            var taskRandom = new Random(_random.Next());            tasks[i] = Task.Run(() =>            {                for (int j = 0; j < iterationsPerTask; j++)                {                    var (user, pwd) = GenerateTestDataForIteration(taskRandom, errorRate);                    var result = _service.LoginWithResult(user, pwd);                    // 不需要额外处理,Result已经包含了成功/失败状态                }            });        }        await Task.WhenAll(tasks);        sw.Stop();        return sw.ElapsedMilliseconds;    }    ///     /// 为单次迭代生成测试数据    ///     ///     ///     ///     private static (string user, string pwd) GenerateTestDataForIteration(Random random, double errorRate)    {        if (random.NextDouble() > errorRate)        {            // 成功案例            return ("valid_user", "correct_password");        }        else        {            // 失败案例 - 随机选择失败类型            if (random.Next(2) == 0)                return ("invalid_user", "any_password");  // 用户不存在            else                return ("valid_user", "wrong_password");   // 密码错误        }    }    ///     /// 预热    ///     ///     ///     ///     private async Task Warmup(int concurrency, double errorRate = 0.3)    {        var warmupTasks = new Task[Math.Min(concurrency, 4)];        for (int i = 0; i < warmupTasks.Length; i++)        {            warmupTasks[i] = Task.Run(() =>            {                var taskRandom = new Random(_random.Next());                for (int j = 0; j < 100; j++)                {                    var (user, pwd) = GenerateTestDataForIteration(taskRandom, errorRate);                    try                    {                        _service.LoginWithException(user, pwd);                    }                    catch { }                    _service.LoginWithResult(user, pwd);                }            });        }        await Task.WhenAll(warmupTasks);        GC.Collect();        GC.WaitForPendingFinalizers();        GC.Collect();    }}
复制代码
并发测试结果:

上述测试可能并不严谨和权威,但是暴露出来的问题还是非常明显的:

  • 在100%失败场景下,Result比Exception快了差不多200倍
  • 接近实际业务场景的30%错误率情况下,Result比Exception也快了160多倍
  • 并发场景下,性能差距也有接近百倍
四、为什么异常在业务场景下如此"昂贵"?

4.1 CLR异常机制的底层原理

4.1.1 异常对象的构造过程

当我们在C#中抛出异常时,看似简单的一行代码,背后却发生了大量复杂的操作:
  1. throw new BusinessException("用户不存在");
复制代码
这个操作的实际执行流程如下:
  1. // 伪代码展示异常构造的实际开销public static Exception CreateException(string message){    // 1. 堆分配:异常对象本身(至少40-64字节)    var exception = RuntimeHelpers.AllocateException(typeof(BusinessException));        // 2. 字段初始化(调用构造函数链)    exception._message = message;  // 字符串分配    exception._stackTrace = null;    exception._innerException = null;    exception._helpURL = null;    exception._source = null;        // 3. 捕获调用堆栈(最昂贵的部分!)    exception.CaptureStackTrace();        return exception;}private void CaptureStackTrace(){    // 4. 获取当前线程的调用堆栈    var frames = new StackFrame[64];  // 分配数组    var frameCount = StackTraceHelper.CaptureStackTrace(        frames, 0,  // 起始位置        false,      // 是否需要文件信息        null);      // 异常对象本身        // 5. 格式化成字符串(可能涉及大量字符串操作)    this._stackTrace = FormatStackTrace(frames, frameCount);}
复制代码
4.1.2 堆栈跟踪的真实代价

让我们深入看看CaptureStackTrace到底做了什么:
  1. // Windows上的实际实现(简化)internal static unsafe int CaptureStackTrace(    StackFrame[] frames,     int startIndex,    bool needFileInfo,    Exception exception){    // 1. 调用系统API获取当前线程的上下文    CONTEXT context;    RtlCaptureContext(&context);        // 2. 遍历调用堆栈(性能杀手!)    STACKFRAME64 stackFrame = new STACKFRAME64();    while (StackWalk64(        IMAGE_FILE_MACHINE_AMD64,        GetCurrentProcess(),        GetCurrentThread(),        &stackFrame,        &context,        null,        SymFunctionTableAccess64,        SymGetModuleBase64,        null))    {        // 3. 解析每个栈帧的信息        frames[frameCount++] = new StackFrame(            stackFrame.AddrPC.Offset,            needFileInfo ? GetSourceInfo(stackFrame) : null);                if (frameCount >= frames.Length) break;    }        return frameCount;}
复制代码
关键点:

  • 每个throw操作都要遍历整个调用堆栈
  • 堆栈遍历涉及多个系统调用和内存访问
  • 需要将内存地址解析为方法名、文件名、行号等
  • 在Release模式下,JIT优化可能会影响堆栈信息
4.1.3 JIT和AOT编译对异常的影响
  1. // 考虑以下代码public int Process(int value){    try    {        return ProcessValue(value);  // 可能抛出异常    }    catch (Exception)    {        return -1;    }}// JIT编译器需要生成:// 1. 正常执行路径的代码// 2. 异常处理表(EH表)// 3. 堆栈展开代码// 4. finally块执行逻辑(如果有)
复制代码
EH表的结构:
  1. Method Exception Handling Table:Start   Length  Handler Type    Class           Filter0x0000  0x0020  0x0030  CLAUSE  Exception       null
复制代码
每个try-catch块都会在方法的元数据中添加EH表条目,增加方法大小和加载时间。
4.2 异常处理的内存分配细节

4.2.1 异常对象的内存布局
  1. // Exception类的简化内存布局class Exception{    // 对象头(8-16字节)    MethodTable* _methodTable;  // 8字节    // 同步块索引(可选)        // 实例字段    string _message;           // 8字节(引用)    IDictionary _data;         // 8字节(通常为null)    Exception _innerException; // 8字节    string _helpURL;          // 8字节    string _source;           // 8字节    string _stackTrace;       // 8字节(字符串,实际分配更大)    object _stackTraceString; // 8字节(可能不同格式)    object _remoteStackTrace; // 8字节    int _remoteStackIndex;    // 4字节    int _HResult;             // 4字节        // 总共:至少80字节(64位系统)    // 加上字符串内容:可能数百到数千字节}
复制代码
4.2.2 GC的影响
  1. // 高频抛出异常会显著影响GCpublic void TestExceptionGC(){    var list = new List();        for (int i = 0; i < 10000; i++)    {        try        {            throw new Exception($"Error {i}");        }        catch (Exception ex)        {            list.Add(ex);  // 大量对象进入第0代堆        }    }}
复制代码
上述代码会导致:

  • 触发频繁的Gen0 GC
  • 如果ex被长时间引用,可能进入Gen1/Gen2
  • 增加GC暂停时间
  • 降低缓存局部性
4.3 CPU级别的性能影响

4.3.1 现代CPU的异常处理开销
  1. ; x64汇编层面的异常处理; 正常路径:process_value:    mov eax, [rcx]      ; 加载值    add eax, 100        ; 计算    ret                 ; 返回    ; 异常路径:throw_exception:    ; 1. 保存所有寄存器到堆栈    push rbx    push rbp    push r12    push r13    push r14    push r15    sub rsp, 28h        ; 分配堆栈空间        ; 2. 调用异常构造函数    call Exception..ctor        ; 3. 设置SEH(结构化异常处理)    mov [rsp+20h], rcx  ; 保存异常对象    call __CxxThrowException@8        ; 4. 清理堆栈    add rsp, 28h    pop r15    pop r14    pop r13    pop r12    pop rbp    pop rbx
复制代码
CPU层面的问题:

  • 分支预测失败:异常路径很少执行,CPU分支预测器难以优化
  • 缓存失效:异常处理代码通常不在指令缓存中
  • 流水线停顿:异常处理需要保存/恢复大量寄存器状态
  • 内存访问模式差:EH表查找导致随机内存访问
4.3.2 对比正常返回和异常返回
  1. // Result的正常返回路径return Result.Fail("用户不存在");// 汇编:; 1. 构造Result对象(可能在栈上); 2. 设置Success=false; 3. 设置Error字段; 4. 返回(普通ret指令)// 异常返回路径throw new BusinessException("用户不存在");// 汇编:; 1. 堆分配异常对象; 2. 捕获堆栈跟踪; 3. 设置SEH帧; 4. 调用kernel32!RaiseException; 5. 堆栈展开; 6. 查找catch块; 7. 执行catch块代码
复制代码
4.4 对比其他编程语言

4.4.1 Java的异常机制
  1. // Java的异常使用看起来和C#相似public User login(String username, String password)     throws UserNotFoundException, InvalidPasswordException{    User user = findUser(username);    if (user == null) {        throw new UserNotFoundException("用户不存在");    }    if (!verifyPassword(user, password)) {        throw new InvalidPasswordException("密码错误");    }    return user;}
复制代码
Java异常的特点:
1.检查型异常(Checked Exception):强制处理或声明
2.性能开销与C#类似:同样需要捕获堆栈跟踪
3.JVM的优化:HotSpot JVM有更成熟的异常优化:

  • 内联缓存(Inline Cache)
  • 栈上替换(On-Stack Replacement)
  • 但业务异常仍然昂贵
重要区别:
  1. // Java 14+引入了Records,但异常开销依旧public record Result(T value, String error) {    public boolean isSuccess() { return error == null; }}// Java社区也在转向Result模式,特别是响应式编程public Mono login(String username, String password) {    return Mono.fromCallable(() -> findUser(username))        .switchIfEmpty(Mono.error(new UserNotFoundException()))        .filter(user -> verifyPassword(user, password))        .switchIfEmpty(Mono.error(new InvalidPasswordException()));}
复制代码
4.4.2 Go的错误处理哲学
  1. // Go的错误处理:显式返回错误func Login(username, password string) (*User, error) {    user, err := findUser(username)    if err != nil {        return nil, fmt.Errorf("查找用户失败: %w", err)    }        if !verifyPassword(user, password) {        return nil, errors.New("密码错误")    }        return user, nil}// 调用方必须显式处理错误user, err := Login("test", "123")if err != nil {    // 处理错误    switch {    case strings.Contains(err.Error(), "密码错误"):        // 特定处理    default:        // 通用处理    }}
复制代码
Go的设计选择:

  • 没有异常机制:只有错误返回值
  • 错误是值(errors are values):可以像普通值一样传递
  • 强制显式处理:无法忽略错误(除非使用_)
  • 零成本抽象:错误处理几乎没有运行时开销
Go的错误性能优势:
  1. // Go的errors.New实际上很简单func New(text string) error {    return &errorString{text}}type errorString struct {    s string}func (e *errorString) Error() string {    return e.s}// 没有堆栈跟踪,没有复杂构造// 只是一个包含字符串的结构体
复制代码
4.4.3 Rust的Result类型
  1. // Rust的错误处理:基于枚举的Resultfn login(username: &str, password: &str) -> Result {    let user = find_user(username)?;  // ?操作符自动传播错误        if !verify_password(&user, password) {        return Err(LoginError::InvalidPassword);    }        Ok(user)}// 错误类型定义#[derive(Debug)]enum LoginError {    UserNotFound,    InvalidPassword,    DatabaseError(DbError),}// 使用match处理match login("test", "123") {    Ok(user) => println!("欢迎 {}", user.name),    Err(LoginError::UserNotFound) => println!("用户不存在"),    Err(LoginError::InvalidPassword) => println!("密码错误"),    Err(e) => println!("其他错误: {:?}", e),}
复制代码
Rust的设计特点:

  • 零成本抽象:Result在运行时通常是普通枚举
  • 模式匹配:编译器确保所有情况都被处理
  • 错误传播运算符(?):简化错误传播
  • 丰富的错误库:anyhow、thiserror等
Rust的性能优势:
  1. 编译后的Result通常优化为:1.成功:存储User2.失败:存储错误码(通常是整数)3.没有堆分配,没有虚函数调用
复制代码
4.4.4 C++的错误处理
  1. // C++的多种错误处理方式// 1. 异常(类似C#/Java)User login(const std::string& username, const std::string& password) {    auto user = find_user(username);    if (!user) {        throw UserNotFoundException("用户不存在");    }    if (!verify_password(*user, password)) {        throw InvalidPasswordException("密码错误");    }    return *user;}// 2. 错误码(传统方式)int login(const std::string& username,           const std::string& password,          User& out_user) {    User user;    int err = find_user(username, user);    if (err != 0) return err;        if (!verify_password(user, password)) {        return ERROR_INVALID_PASSWORD;    }        out_user = user;    return 0;  // 成功}// 3. std::expected(C++23)std::expected login(const std::string& username,                                 const std::string& password) {    auto user = find_user(username);    if (!user) {        return std::unexpected(Error::UserNotFound);    }    if (!verify_password(*user, password)) {        return std::unexpected(Error::InvalidPassword);    }    return *user;}
复制代码
C++的选择:

  • 游戏和嵌入式:通常禁用异常,使用错误码
  • 性能敏感应用:避免异常,因为零开销原则
  • 现代C++:倾向于std::expected等类型安全方案
4.5 .NET Core的改进和局限

.Net 8.0版本,官方团队针对异常处理这块进行了大幅的优化,包括预分配异常对象(PREallocated Exception)、延迟堆栈跟踪生成(Lazy Stack Trace)、堆栈跟踪缓存和复用、新的堆栈跟踪算法等多种手段,同时也对JIT和RunTime进行了针对性优化。但是目前来看,依旧还有很大的提升空间。
详情请见官方团队的博客:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#exception-handling
  1. // .NET 8.0 对参数异常的特殊优化public void ValidateUser(string username, int age){    // 这些调用在 .NET 8.0 中非常高效    ArgumentNullException.ThrowIfNull(username);    ArgumentOutOfRangeException.ThrowIfNegative(age);    ArgumentException.ThrowIfNullOrEmpty(username);        // 但注意:业务异常不在此优化范围内!    if (!IsValidUsername(username))        throw new BusinessException("无效用户名"); // 代价仍然昂贵}
复制代码
五、Result的进阶优势

5.1 丰富的错误信息
  1. public class Result{    public bool Success { get; }    public T? Value { get; }    public string ErrorCode { get; }    // 错误代码    public string ErrorMessage { get; } // 错误消息    public Dictionary Metadata { get; } // 附加信息}// 使用:var result = Login("test", "wrong");if (!result.Success){    switch (result.ErrorCode)    {        case "USER_NOT_FOUND":            // 用户不存在,跳转注册页面            break;        case "INVALID_PASSWORD":            // 密码错误,显示提示            break;        case "ACCOUNT_LOCKED":            // 账户锁定,显示锁定时间            var lockTime = result.Metadata["LockUntil"];            break;    }}
复制代码
5.2 函数式编程支持
  1. // Map - 转换成功的值var userResult = GetUser(123);var userName = userResult.Map(user => user.Name.ToUpper());// Bind - 链式操作var orderResult = GetUser(123)    .Bind(user => GetOrder(user.CurrentOrderId))    .Bind(order => ValidateOrder(order));// Match - 模式匹配var message = loginResult.Match(    success: user => $"欢迎回来,{user.Name}!",    failure: error => $"登录失败: {error.Message}");
复制代码
5.3 更好的API设计
  1. // Web API中的使用[HttpPost("login")]public IActionResult Login([FromBody] LoginRequest request){    var result = _authService.Login(request);        return result.Match(        success: user => Ok(new { success = true, user }),        failure: error => BadRequest(new {             success = false,             errorCode = error.Code,            message = error.Message         })    );}// 客户端获得清晰的响应:// 成功: { success: true, user: { ... } }// 失败: { success: false, errorCode: "INVALID_PASSWORD", message: "密码错误" }
复制代码
六、 Result模式的一些弊端

6.1 "if地狱"问题(条件判断泛滥)

最常被诟病的问题,就是代码中充斥大量的 if (!result.Success) 检查。
  1. // "if地狱"的典型例子public Result ProcessOrder(int userId, OrderRequest request){    var userResult = GetUser(userId);    if (!userResult.Success)        return Result.Fail(userResult.Error);        var validationResult = ValidateOrder(request);    if (!validationResult.Success)        return Result.Fail(validationResult.Error);        var inventoryResult = CheckInventory(request.Items);    if (!inventoryResult.Success)          return Result.Fail(inventoryResult.Error);        var paymentResult = ProcessPayment(userResult.Value, request);    if (!paymentResult.Success)        return Result.Fail(paymentResult.Error);        // ... 更多检查}
复制代码
这个问题,使用函数式编程思想可以巧妙解决,这一块JAVA做的真心挺不错的。
  1. // 使用 Railway-Oriented Programmingpublic Result ProcessOrder(int userId, OrderRequest request){    return GetUser(userId)        .Bind(user => ValidateOrder(request)            .Bind(validated => CheckInventory(request.Items)                .Bind(inventory => ProcessPayment(user, request)                    .Map(payment => CreateOrder(user, validated, payment)))));}// 或者使用扩展方法public Result ProcessOrder(int userId, OrderRequest request){    return GetUser(userId)        .Then(user => ValidateOrder(request))        .Then(validated => CheckInventory(request.Items))        .Then(inventory => ProcessPayment(user, request))        .Map(payment => CreateOrder(user, validated, payment));}
复制代码
6.2 值类型(struct)vs 引用类型(class)的两难选择
  1. public readonly struct Result  // 值类型{    private readonly T? _value;    private readonly string? _error;        // 问题1:T是引用类型时,struct存储的是引用    // 问题2:struct复制开销(如果T是大对象)    // 问题3:装箱/拆箱开销    // 问题4:不能为null,需要额外状态标记}public class Result  // 引用类型{    // 问题:每个Result都是堆分配,增加GC压力    // 即使成功情况也要分配对象}
复制代码
折中方案:针对值类型和引用类型分别优化,针对通用的Result对象进行缓存和复用。
  1. /// /// 结果基类/// /// public abstract class Result{    ///     /// 是否成功    ///     public abstract bool IsSuccess { get; }    ///     /// 具体返回值(成功时有效)    ///     public abstract T? Value { get; }    ///     /// 错误码    ///     public abstract string? ErrorCode { get; }    ///     /// 错误信息    ///     public abstract string? ErrorMessage { get; }      // 缓存单例成功(仅针对 default(T))    private static readonly ConcurrentDictionary _successCache = new();    //只按 errorCode 缓存(避免按 message 无限增长)    private static readonly ConcurrentDictionary _errorCache = new();  } /// /// 仅用于性能关键路径,不存储大对象/// /// public readonly ref struct ValueResult where T : struct{  //省略} ///  /// 引用类型结果,仅用于性能关键路径 ///  ///  public readonly ref struct RefResult where T : class{ //省略}
复制代码
6.3 异步编程的复杂性

在异步编程中,可能每个异步操作都需要处理Result,这也大大增加了代码复杂度,其实也属于上面说的“if 地狱”范畴。
  1. public async Task LoginAsync(string username, string password){    // 每个异步操作都需要处理Result    var userResult = await FindUserAsync(username);    if (!userResult.Success)        return Result.Fail(userResult.Error);        var validationResult = await ValidatePasswordAsync(userResult.Value, password);    if (!validationResult.Success)        return Result.Fail(validationResult.Error);        return Result.Ok(userResult.Value);}// 对比异常版本:public async Task LoginAsync(string username, string password){    var user = await FindUserAsync(username);  // 抛异常则直接中断后续逻辑    await ValidatePasswordAsync(user, password); // 抛异常则直接中断后续逻辑    return user;}
复制代码
6.4 类型系统冗长

每一个接口方法都要包裹Result,再加上异步的Task,分页请求结果模型PagedResult,再加上点其他东西,就会出现令人头皮发麻的泛型参数爆炸。
下面的代码,是项目里面的真实代码
  1.      ///     /// 员工业务服务接口    ///     public interface IEmployeeService    {        Task CreateAsync(CreateEmployeeDto dto);        Task GetByIdAsync(long id);        Task UpdateAsync(UpdateEmployeeDto dto);        Task DeleteAsync(long id);        Task GetPageListAsync(EmployeePageListDto dto);        Task GetListAsync(string? keyword = null);        Task GetEmployeeAliases(List? employeeIds = null, bool includeShowName = true);    }
复制代码
6.5 其他问题

当然,这种模式还有一些其他的问题,比如团队成员的接受度,团队学习成本,与现有代码/生态的兼容性,与第三方包的兼容性等,这里就不一一说明了。
七、常见问题与答疑

Q:异常不是更方便吗?一行代码就能中断流程
A:方便不等于正确。goto语句也很"方便",但现代编程中我们避免使用它。异常在业务逻辑中就是"远程goto",破坏了代码的可读性和可维护性。
Q:Result需要更多的if判断,代码更冗长
  1. // 简洁的处理方式var result = Login("test", "123");if (!result.Success) return result;// 或者使用模式匹配var message = result switch{    { Success: true, Value: var user } => $"欢迎 {user.Name}",    { Error: var error } => $"错误: {error}",    _ => "未知状态"};
复制代码
Q:我们的项目很小,性能影响不大
A:即使不考虑性能,从代码质量和维护性角度,Result也是更好的选择。好的习惯应该从项目初期就开始培养。
八、总结

Result 不是银弹,它有它适用的场景,也有相应的一些弊端。选择的关键不在于哪个"更好",而在于哪个"更适合"当前的场景和约束。明智的工程师会根据具体情况做出平衡的选择。
适合使用 Result 的场景:

  • 高频失败的校验逻辑(表单验证、业务规则检查)
  • 需要明确错误分类的业务流程
  • API边界(需要结构化错误响应)
  • 与外部系统交互(需要处理各种失败模式)
  • 需要组合的复杂业务逻辑
仍然适合使用异常的场景:

  • 真正的系统故障(内存不足、数据库崩溃)
  • 程序状态异常(空引用、索引越界)
  • 不满足前置条件(无效参数)
  • 开发阶段的断言检查
  • 极低失败率的操作
关键建议:

  • 不要全盘替换:Result和异常各有适用场景
  • 分层设计:不同架构层使用不同策略
  • 团队共识:建立明确的规范和边界
  • 渐进采用:从核心业务开始,逐步扩展
  • 监控反馈:通过日志和监控验证选择
最终原则:

  • 异常:用于"不应该发生"的事情
  • Result:用于"可能发生但需要处理"的事情

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

15 小时前

举报

您需要登录后才可以回帖 登录 | 立即注册