告别 throw exception!为什么 Result<T> 才是业务逻辑的正确选择
引言:一个普遍存在的“坏味道”
如果你在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
// 简洁的处理方式
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:用于"可能发生但需要处理"的事情

浙公网安备 33010602011771号