博客 / 詳情

返回

告別 throw exception!為什麼 Result<T> 才是業務邏輯的正確選擇

目錄
  • 引言:一個普遍存在的“壞味道”
  • 一、異常的“原罪” —— 我們一直在濫用它
    • 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#項目中看到這樣的代碼,一定不會感到陌生:

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模式編譯):

image

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();
    }
}

併發測試結果:
image

上述測試可能並不嚴謹和權威,但是暴露出來的問題還是非常明顯的:

  • 在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:用於"可能發生但需要處理"的事情
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.