Skip to content

20.7 panic safety

在利用 unsafe 写库的时候,还需要注意的一点是“panic 安全”。panic 在什么情况下发生是难以预测的,我们要做的是,即便在发生 panic 的时候,也能保证“内存安全”。

我们以 Vec::truncate 方法为例来说明“panic 安全”是怎么做到的。这个方法用于把数组切掉一部分,只保留前面的部分,后面的部分扔掉。所以,我们可以想到的实现逻辑应该是针对被切掉的部分,每个元素调用一下析构函数,最后重新设置一下数组的长度大小即可。

可惜这么做是不对的。因为“对象的析构函数”是用户自定义的行为,在这个方法里面会执行什么逻辑是无法提前确定的。所以,我们应该假设它有可能发生 panic。如果有对象已经执行了析构,但是还继续把它留在数组里面,等待数组最后来重新设置长度,是有风险的。所以标准库里面的代码是这么做的:每次执行析构前先把数组长度减 1,从逻辑上将元素从数组中移除,然后执行析构函数。源码如下所示:


rust
pub fn truncate(&mut self, len: usize) {
    unsafe {
        // drop any extra elements
        while len < self.len {
            // decrement len before the drop_in_place(), so a panic on Drop
            // doesn't re-drop the just-failed value.
            self.len -= 1;
            let len = self.len;
            ptr::drop_in_place(self.get_unchecked_mut(len));
        }
    }
}

所以,大家在读源码的时候,不仅要看到别人是这样写的,更要看到别人为什么不会那样写。这段代码看起来好像不够优化,在每次循环的时候减 1,却没有在循环前或者后面一次性减掉 len。这样做是有原因的。请读者仔细理解源码中的那条注释。同样的道理,类似的保障异常安全的设计,我们还可以在 extend_with 等方法中看到。这个方法是实现 resize、resize_default 等方法的基础。


rust
impl<T> Vec<T> {
    /// Extend the vector by `n` values, using the given generator.
    fn extend_with<E: ExtendWith<T>>(&mut self, n: usize, value: E) {
        self.reserve(n);
        unsafe {
            let mut ptr = self.as_mut_ptr().offset(self.len() as isize);
            // Use SetLenOnDrop to work around bug where compiler
            // may not realize the store through `ptr` through self.set_len()
            // don't alias.
            let mut local_len = SetLenOnDrop::new(&mut self.len);

            // Write all elements except the last one
            for _ in 1..n {
                ptr::write(ptr, value.next());
                ptr = ptr.offset(1);
                // Increment the length in every step in case next() panics
                local_len.increment_len(1);
            }

            if n > 0 {
                // We can write the last element directly without cloning needlessly
                ptr::write(ptr, value.last());
                local_len.increment_len(1);
            }

            // len set by scope guard
        }
    }
}

这个方法是往 Vec 后面继续扩展 n 个元素,这 n 个元素可以是通过一个元素 clone()而来,也可以是调用 Default::default()构造而来。

大家可以注意到,在真正写入数据之前,先创建了一个 SetLenOnDrop 类型的变量 local_len,另外每次写入一个新的元素之后,都会将这个变量重新设置长度。而这个 SetLenOnDrop 类型的主要功能,就是在析构的时候修改 Vec 的真正长度。因为在这段 unsafe 代码中,需要调用 value.next()方法,而这个方法最后会调用到元素的 T::default()或者 T::clone()方法。这些方法的实现,取决于外部元素类型的实现,它们会不会导致 panic,写容器的作者是不知道的。因此,Vec 容器的作者只能假定这些外部方法是有 panic 风险的。为了保证在发生 panic 之后 Vec 内部包含的依然是合法数据,一定要每次成功写入一个元素之后,马上更新长度信息。

Released under the MIT License