将告别 throw exception!为什么 Result 这句话可能是在讨论编程中异常处理和返回结果(Result)的概念。1. **将告别 throw exception**:这里的throw exception指的是在编程中抛出异常(Excepti

摘要:引言:一个普遍存在的“坏味道” 如果你在C#项目中看到这样的代码,一定不会感到陌生: public User Login(string username, string password) { var user = FindUser(use
引言:一个普遍存在的“坏味道” 如果你在C#项目中看到这样的代码,一定不会感到陌生: 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)的设计初衷: // 这些才是异常真正的使用场景: 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类如下: public class Result<T> { 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<T> Ok(T value) => new(value); public static Result<T> Fail(string error) => new(error); } 2.2 如何正确使用Result? public Result<User> Login(string username, string password) { if (string.IsNullOrEmpty(username)) return Result<User>.Fail("用户名不能为空"); // ✅ 明确返回业务错误 if (string.IsNullOrEmpty(password)) return Result<User>.Fail("密码不能为空"); // ✅ 明确返回业务错误 var user = FindUser(username); if (user == null) return Result<User>.Fail("用户不存在"); // ✅ 明确返回业务错误 if (!VerifyPassword(user, password)) return Result<User>.Fail("密码错误"); // ✅ 明确返回业务错误 return Result<User>.Ok(user); // ✅ 明确返回成功 } 三、性能对决 —— 几近碾压的性能差距 3.1 部分测试代码 项目环境: <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>latest</LangVersion> </PropertyGroup> </Project> 部分测试代码: public class LoginService { private readonly Dictionary<string, User> _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<User> LoginWithResult(string username, string password) { if (!_users.TryGetValue(username, out var user)) return Result<User>.Fail("用户不存在"); if (password != "correct_password") return Result<User>.Fail("密码错误"); return Result<User>.Ok(user); } } 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 并发场景下:性能差距依旧不忍直视 并发测试核心代码: /// <summary> /// // 并发测试结果类 /// </summary> 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; } } /// <summary> /// 并发测试器 /// </summary> 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"); } } /// <summary> /// 并发异常测试 /// </summary> /// <param name="concurrency"></param> /// <param name="totalIterations"></param> /// <param name="errorRate"></param> /// <returns></returns> private async Task<long> 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; } /// <summary> /// 并发Result测试 /// </summary> /// <param name="concurrency"></param> /// <param name="totalIterations"></param> /// <param name="errorRate"></param> /// <returns></returns> private async Task<long> 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; } /// <summary> /// 为单次迭代生成测试数据 /// </summary> /// <param name="random"></param> /// <param name="errorRate"></param> /// <returns></returns> 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"); // 密码错误 } } /// <summary> /// 预热 /// </summary> /// <param name="concurrency"></param> /// <param name="errorRate"></param> /// <returns></returns> 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#中抛出异常时,看似简单的一行代码,背后却发生了大量复杂的操作: throw new BusinessException("用户不存在"); 这个操作的实际执行流程如下: // 伪代码展示异常构造的实际开销 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到底做了什么: // 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编译对异常的影响 // 考虑以下代码 public int Process(int value) { try { return ProcessValue(value); // 可能抛出异常 } catch (Exception) { return -1; } } // JIT编译器需要生成: // 1. 正常执行路径的代码 // 2. 异常处理表(EH表) // 3. 堆栈展开代码 // 4. finally块执行逻辑(如果有) EH表的结构: Method Exception Handling Table: Start Length Handler Type Class Filter 0x0000 0x0020 0x0030 CLAUSE Exception null 每个try-catch块都会在方法的元数据中添加EH表条目,增加方法大小和加载时间。 4.2 异常处理的内存分配细节 4.2.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的影响 // 高频抛出异常会显著影响GC public void TestExceptionGC() { var list = new List<Exception>(); 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的异常处理开销 ; 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 对比正常返回和异常返回 // Result<T>的正常返回路径 return Result<User>.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的异常机制 // 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) 但业务异常仍然昂贵 重要区别: // Java 14+引入了Records,但异常开销依旧 public record Result<T>(T value, String error) { public boolean isSuccess() { return error == null; } } // Java社区也在转向Result模式,特别是响应式编程 public Mono<User> 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的错误处理哲学 // 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的错误性能优势: // 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类型 // Rust的错误处理:基于枚举的Result fn login(username: &str, password: &str) -> Result<User, LoginError> { 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的性能优势: 编译后的Result通常优化为: 1.成功:存储User 2.失败:存储错误码(通常是整数) 3.没有堆分配,没有虚函数调用 4.4.4 C++的错误处理 // 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<User, Error> 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 // .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 丰富的错误信息 public class Result<T> { public bool Success { get; } public T? Value { get; } public string ErrorCode { get; } // 错误代码 public string ErrorMessage { get; } // 错误消息 public Dictionary<string, object> 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 函数式编程支持 // 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设计 // Web API中的使用 [HttpPost("login")] public IActionResult Login([FromBody] LoginRequest request) { var result = _authService.Login(request); return result.Match<IActionResult>( 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) 检查。 // "if地狱"的典型例子 public Result<Order> ProcessOrder(int userId, OrderRequest request) { var userResult = GetUser(userId); if (!userResult.Success) return Result<Order>.Fail(userResult.Error); var validationResult = ValidateOrder(request); if (!validationResult.Success) return Result<Order>.Fail(validationResult.Error); var inventoryResult = CheckInventory(request.Items); if (!inventoryResult.Success) return Result<Order>.Fail(inventoryResult.Error); var paymentResult = ProcessPayment(userResult.Value, request); if (!paymentResult.Success) return Result<Order>.Fail(paymentResult.Error); // ... 更多检查 } 这个问题,使用函数式编程思想可以巧妙解决,这一块JAVA做的真心挺不错的。 // 使用 Railway-Oriented Programming public Result<Order> 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<Order> 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)的两难选择 public readonly struct Result<T> // 值类型 { private readonly T? _value; private readonly string? _error; // 问题1:T是引用类型时,struct存储的是引用 // 问题2:struct复制开销(如果T是大对象) // 问题3:装箱/拆箱开销 // 问题4:不能为null,需要额外状态标记 } public class Result<T> // 引用类型 { // 问题:每个Result都是堆分配,增加GC压力 // 即使成功情况也要分配对象 } 折中方案:针对值类型和引用类型分别优化,针对通用的Result对象进行缓存和复用。 /// <summary> /// 结果基类 /// </summary> /// <typeparam name="T"></typeparam> public abstract class Result<T> { /// <summary> /// 是否成功 /// </summary> public abstract bool IsSuccess { get; } /// <summary> /// 具体返回值(成功时有效) /// </summary> public abstract T? Value { get; } /// <summary> /// 错误码 /// </summary> public abstract string? ErrorCode { get; } /// <summary> /// 错误信息 /// </summary> public abstract string? ErrorMessage { get; } // 缓存单例成功(仅针对 default(T)) private static readonly ConcurrentDictionary<Type, Result<T>> _successCache = new(); //只按 errorCode 缓存(避免按 message 无限增长) private static readonly ConcurrentDictionary<string, Result<T>> _errorCache = new(); } /// <summary> /// 仅用于性能关键路径,不存储大对象 /// </summary> /// <typeparam name="T"></typeparam> public readonly ref struct ValueResult<T> where T : struct { //省略 } /// <summary> /// 引用类型结果,仅用于性能关键路径 /// </summary> /// <typeparam name="T"></typeparam> public readonly ref struct RefResult<T> where T : class { //省略 } 6.3 异步编程的复杂性 在异步编程中,可能每个异步操作都需要处理Result,这也大大增加了代码复杂度,其实也属于上面说的“if 地狱”范畴。 public async Task<Result<User>> LoginAsync(string username, string password) { // 每个异步操作都需要处理Result var userResult = await FindUserAsync(username); if (!userResult.Success) return Result<User>.Fail(userResult.Error); var validationResult = await ValidatePasswordAsync(userResult.Value, password); if (!validationResult.Success) return Result<User>.Fail(validationResult.Error); return Result<User>.Ok(userResult.Value); } // 对比异常版本: public async Task<User> LoginAsync(string username, string password) { var user = await FindUserAsync(username); // 抛异常则直接中断后续逻辑 await ValidatePasswordAsync(user, password); // 抛异常则直接中断后续逻辑 return user; } 6.4 类型系统冗长 每一个接口方法都要包裹Result,再加上异步的Task,分页请求结果模型PagedResult,再加上点其他东西,就会出现令人头皮发麻的泛型参数爆炸<<<<>>>>。 下面的代码,是项目里面的真实代码 /// <summary> /// 员工业务服务接口 /// </summary> public interface IEmployeeService { Task<Result<long>> CreateAsync(CreateEmployeeDto dto); Task<Result<EmployeeDto>> GetByIdAsync(long id); Task<Result> UpdateAsync(UpdateEmployeeDto dto); Task<Result> DeleteAsync(long id); Task<Result<PagedResult<EmployeeDto>>> GetPageListAsync(EmployeePageListDto dto); Task<Result<List<EmployeeDto>>> GetListAsync(string? keyword = null); Task<Result<Dictionary<string,long>>> GetEmployeeAliases(List<long>? employeeIds = null, bool includeShowName = true); } 6.5 其他问题 当然,这种模式还有一些其他的问题,比如团队成员的接受度,团队学习成本,与现有代码/生态的兼容性,与第三方包的兼容性等,这里就不一一说明了。 七、常见问题与答疑 Q:异常不是更方便吗?一行代码就能中断流程 A:方便不等于正确。goto语句也很"方便",但现代编程中我们避免使用它。异常在业务逻辑中就是"远程goto",破坏了代码的可读性和可维护性。 Q:Result需要更多的if判断,代码更冗长 // 简洁的处理方式 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:用于"可能发生但需要处理"的事情