第 1 章 Result 类型定义与核心思想:泛型设计、Ok/Err 变体、错误码枚举

1.1 设计原理:为什么需要 Result 类型?

在跨模块调用的场景中,错误往往不再局限于当前函数栈。传统的异常抛出机制会导致调用链难以追踪,而返回错误码又容易丢失类型信息。Result 类型通过泛型封装,将成功值和错误值统一在一个类型中,让调用方在编译期就知道需要处理两种路径,从而实现显式错误传播与类型安全。

核心设计目标:消除隐式错误状态,让每一个可能出错的函数都明确返回「成功」或「失败」两种结果,且失败时携带结构化的错误信息。

1.2 变体设计:Ok 与 Err

Result 类型包含两个变体:

变体携带数据语义
Ok(T)成功值(任意类型)操作正常完成,调用方可直接提取值
Err(E)错误信息(错误码或错误对象)操作失败,调用方需检查错误并处理

变体决定了后续的控制流:match 或 if let 可强制开发者为两种可能性编写分支,避免遗漏错误路径。

1.3 泛型参数:T 与 E

T 代表成功值的类型,E 代表错误类型。泛型使得 Result 可以适配任意业务场景,而错误类型 E 通常设计为枚举,以覆盖模块内所有可能的错误原因。

// 常用错误码枚举示例
enum ErrorCode {
    NotFound,
    PermissionDenied,
    InvalidInput,
    Timeout,
    Unknown
}

// 泛型 Result 定义(伪代码,类似 Rust 语法)
enum Result<T, E> {
    Ok(T),
    Err(E)
}

// 使用 Result 的函数示意
fn read_config(path: String) -> Result<Config, ErrorCode> {
    if !file_exists(&path) {
        return Err(ErrorCode::NotFound);
    }
    let data = parse_file(&path);
    // ...
    Ok(Config::new(data))
}

1.4 跨模块契约:错误码枚举的规模化

错误码枚举不应分散定义,而应在模块边界层统一管理。每个模块只需暴露自己的 ErrorCode 枚举,并确保所有 Err 值都来自该枚举。这样上层模块可以:

  • 通过 match 精准处理已知错误;
  • 将未知错误通过 Err(ErrorCode::Unknown) 向上传递;
  • 避免魔法数字和隐式状态。
避坑指南:不要同时使用多个不同的错误类型(例如混合 int 错误码和异常对象)。泛型 E 在一个模块链中应保持固定,否则会破坏类型一致性,导致调用方无法统一处理。

1.5 结构图:Result 类型核心骨架

Result<T, E> 类型结构 Result<T, E> Ok(T) Err(E) 成功值(类型 T) 错误码枚举 (E) 泛型参数 T 和 E 由调用方指定,编译器在 match 时强制所有分支覆盖

1.6 实现流程:定义 Result 与使用 error code

以 C++ 为例,展示一个最小化 Result 实现,包含错误码枚举与基本方法:

// result.hpp
#pragma once
#include <variant>
#include <stdexcept>

enum class ErrorCode {
    NotFound = 1001,
    InvalidInput,
    Timeout,
    Unknown
};

template <typename T, typename E = ErrorCode>
class Result {
public:
    using value_type = T;
    using error_type = E;

    static Result Ok(T val)  { return Result(std::move(val)); }
    static Result Err(E err) { return Result(std::move(err)); }

    bool is_ok()  const { return std::holds_alternative<T>(data_); }
    bool is_err() const { return std::holds_alternative<E>(data_); }

    T& unwrap() {
        if (!is_ok()) throw std::runtime_error("unwrap on Err");
        return std::get<T>(data_);
    }

    E& unwrap_err() {
        if (!is_err()) throw std::runtime_error("unwrap_err on Ok");
        return std::get<E>(data_);
    }

private:
    explicit Result(T val) : data_(std::move(val)) {}
    explicit Result(E err) : data_(std::move(err)) {}
    std::variant<T, E> data_;
};
// 使用示例
#include "result.hpp"
Result<int> divide(int a, int b) {
    if (b == 0) return Result<int>::Err(ErrorCode::InvalidInput);
    return Result<int>::Ok(a / b);
}

int main() {
    auto res = divide(10, 0);
    if (res.is_err()) {
        // 处理错误(此处仅输出)
        printf("Error code: %d\n", static_cast<int>(res.unwrap_err()));
    } else {
        printf("Result: %d\n", res.unwrap());
    }
    return 0;
}

1.7 性能考虑与常见陷阱

  • 对象大小:std::variant 大小等于最大成员大小加索引标记,对于大类型可考虑指针或 unique_ptr 包装。
  • 错误码选择:枚举值应避免为 0,防止与默认初始化混淆。建议从 1001 开始。
  • 跨模块暴露:Result 类型必须放在公共头文件中,且 E 的枚举定义必须在模块边界可见。
最佳实践:在模块入口层统一使用 Result,内部函数可以继续使用异常(如果性能允许),但最后在对外接口处捕获并转换为 Result 返回。这样既享受内部异常的简洁,又保证 API 跨语言的稳定性。

1.8 本章小结