#debugging #unwrap #debug-mode #options

debug_unwraps

仅调试检查的展开扩展特性

1 个不稳定版本

0.1.0 2022年8月23日

#9 in #unwrap

MIT 许可证

12KB
103

debug_unwraps

为 Rust 的 Option 和 Result 类型添加仅调试的展开功能

动机

当编写新的高层结构时,使用 .unwrap().expect() 可以确保内部不变量得到维护。这允许单元和集成测试在代码更改违反某些预期结构时快速失败。

另一方面,在发布模式下,最好去除此类检查,因为预计 API 本身将强制执行不变量。

考虑以下示例结构

/// A silly dag structure which creates a heirachichal group of sets of numbers
struct NumTreeBuilder {
    /// Stack of currently edited nodes
    stack: Vec<usize>,
    /// An adjacency list of children
    nodes: Vec<Vec<usize>>,
    /// List of numbers associated with each node
    data: Vec<Vec<u32>>,
}

impl NumTreeBuilder {
    /// Creates an empty `NumTreeBuilder`
    /// 
    /// Editing will start at the root
    pub fn new() -> Self {
        Self {
            // Editing starts at the `0` node which is the root
            stack: vec![0],
            // Insert an empty list of children for node 0
            nodes: vec![Vec::new()],
            // Default name for the root
            names: vec![Vec::new()],
        }
    }

    /// Add a new number to the set of the currently edited node
    pub fn add_num(&mut self, num: u32) {
        if let Some(current) = self.stack.last() {
            self.data.get_mut(*current)
                .expect("A node ID was on the stack but didn't have associated data")
                .push(num);
        } else {
            unreachable!("The stack should never be empty because the root cannot be popped");
        }
    }

    /// Start a new child under the current node
    pub fn start_child(&mut self) {
        // Create a new child node
        let child_id = self.nodes.len();
        // INVARIANT: Children must have adjacency and data structures created
        // before they can be edited
        self.nodes.push(Vec::new());
        self.data.push(Vec::new());

        // Update the child list of the current node and push the new child
        // onto the edit stack
        if let Some(current) = self.stack.last() {
            self.nodes.get_mut(*current)
                .expect("A node ID was on the stack but didn't have associated data")
                .push(child_id);

            self.stack.push(child_id);
        } else {
            unreachable!("The stack should never be empty because the root cannot be popped");
        }
    }

    /// Stop editing a child and return to its parent
    pub fn finish_child(&mut self) {
        if self.stack.len() > 1 {
            self.stack.pop();
        }
        // INVARIANT: Don't pop if we are at the root
    }
}

API 设计为不允许无效状态,如空栈或缺少节点数据,但我们被迫总是进行运行时检查。目前获取仅调试检查的唯一方法是通过 debug_assert!()

impl NumTreeBuilder {
    /// Start a new child under the current node
    pub fn start_child(&mut self) {
        // Create a new child node
        let child_id = self.nodes.len();
        // INVARIANT: Children must have adjacency and data structures created
        // before they can be edited
        self.nodes.push(Vec::new());
        self.data.push(Vec::new());

        // Edit the parent adjacency list

        debug_assert!(self.stack.last().is_some(), "The stack should never be empty because the root cannot be popped");
        // SAFETY: this invariant should be upheld by the API because `finish_child()` 
        // only pops an item if the stack is greater than 1 item long
        // In debug mode this will panic in the above debug_assert!
        let current = unsafe {self.stack.last().unwrap_unchecked()};

        debug_assert!(self.nodes.get_mut(*current).is_some(), "A node ID was on the stack but didn't have associated data");
        // SAFETY: this invariatn is upheld above because all new child nodes
        // have nodes and data items pushed by `start_child()`
        // In debug mode this will panic in the above debug_assert!
        unsafe { self.nodes.get_mut(*current).unwrap_uncheckd().push(child_id) };

        // Start editing the new child
        self.stack.push(child_id);
    }
}

这有一些缺点,因为它将错误检查与可能生成错误的位置分开。此外,如果每个操作都在自己的 unsafe 块中,则增加审计时需要检查的地点数量。

解决方案

此 crate 为 Option<T>Result<T,E> 提供扩展特性,在编译时带有 debug-assertions 条件性地启用调试。

impl NumTreeBuilder {
    /// Start a new child under the current node
    pub fn start_child(&mut self) {
        // Use extension trait
        use debug_unwraps::DebugUnwrapExt;

        // Create a new child node
        let child_id = self.nodes.len();

        // INVARIANT: Children must have adjacency and data structures created
        // before they can be edited
        self.nodes.push(Vec::new());
        self.data.push(Vec::new());

        // SAFETY: The invariants that the stack is not empty and all nodes have
        // an adjacency list are upheld by the rest of the API. In debug mode
        // these invariants will be checked
        unsafe {
            let current = self.stack.last().debug_expect_unchecked("The stack should never be empty because the root cannot be popped");
            self.nodes.get_mut(*current)
                .debug_expect_unchecked("A node ID was on the stack but didn't have associated data")
                .push(child_id);
        }

        // Start editing the new child
        self.stack.push(child_id);
    }
}

使用此代码,错误检查再次是内联的,并且在重构期间可以捕获单元/集成测试中的失败。但在发布版本中,代码不会进行任何检查。

无运行时依赖