1 个不稳定版本
0.1.0 | 2022年8月23日 |
---|
#9 in #unwrap
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);
}
}
使用此代码,错误检查再次是内联的,并且在重构期间可以捕获单元/集成测试中的失败。但在发布版本中,代码不会进行任何检查。