RAII 模式的 Rust 解决方案,用于在 new() 和 drop() 上修改另一个对象

问题描述 投票:0回答:2

在 C++ 中有这样的安排是非常标准的:

// Some object that you want to manipulate
class DataType 
{ 
public: 
    foo() {} 
    bar() {} 
};

// Some class that manipulates a DataType on construct and destruct
// Your typical RAII scope side effect type thing
class DoFooAndBar
{
public:
    DoFooAndBar(DataType& dataType) : m_dataType(dataType)
    { m_dataType.foo(); }

    ~DoFooAndBar() 
    { m_dataType.bar(); }

private:
    DataType& m_dataType
}

我尝试在 Rust 中做这样的事情,但我遇到了 DoFooAndBar 采用可变引用的问题,然后后续代码无法采用不可变引用。我可以用你典型的

Rc::RefCell
hack 来处理这个问题,但我想知道是否有一种正确的方法来做这样的事情而不诉诸于那个。

这是我的 Rust 代码,为这个问题尽可能地简化了代码:

/// Symbol table example to for a hypothetical script interpreter
/// Simplified to demonstrate issues with borrowing references
use std::collections::HashMap;

/// For our purposes a frame from is just a string->string map
type SymbolFrame = HashMap<String, String>;

/// Our stack of frames is just a vector of stack frame
/// Not performant, but that's not the point
type SymbolFrameStack = Vec<SymbolFrame>;

/// The SymbolTable type contains a stack of symbol frames
/// and routines for working with them
pub struct SymbolTable
{
    frames: SymbolFrameStack
}
impl SymbolTable
{
    /// Start with an initial stack with one frame
    pub fn new() -> Self { SymbolTable { frames: vec![SymbolFrame::new()] } }

    /// Push and pop frames
    pub fn push(&mut self) { self.frames.push(SymbolFrame::new()); }
    pub fn pop(&mut self) { self.frames.pop(); }

    /// See if a symbol exists by name anywhere in the frames
    pub fn contains(&self, name: &str) -> bool {
        for frame in self.frames.iter() { 
            if frame.contains_key(name) {
                return true;
            }
        }
        return false;
    }
}

/// Push a frame on new(); pop the frame on drop()
pub struct SymbolStacker<'a> { table: &'a mut SymbolTable }
impl<'a> SymbolStacker<'a>
{
    pub fn new(table: &'a mut SymbolTable) -> SymbolStacker {
        table.push();
        SymbolStacker { table }
    }
}
impl<'a> Drop for SymbolStacker<'a> {
    fn drop(&mut self) {
        self.table.pop();
    }
}

#[test]
fn bad_test_symbol_table() {
    // Create our table for testing
    let table = &mut SymbolTable::new();
    {
        // Enter a new scope of code, pushing a new stack frame
        let _stacker1 = SymbolStacker::new(table);
        {
            // ...a lot of other recursive code
            // ...presumably passing the table struct around in some way
            // ...we just try to see if a symbol exists in the table
            assert!(!table.contains("foo"));
        }

        assert!(table.contains("foo"));
    }
}

运行测试时会产生编译错误:

error[E0502]: cannot borrow `*table` as immutable because it is also borrowed as mutable

我明白这个错误,我只是在寻找关于如何最好地在 Rust 中做这种事情的建议。

c++ rust raii
2个回答
2
投票

我知道这不是你要找的答案,但我认为这个问题的最佳答案是“在 Rust 中以这种方式编写代码很混乱,因为 Rust 并非设计为以这种方式使用。”在 Rust 中使用 RAII 范围的副作用表明你是一名 C++ 开发人员,试图在 Rust 中编写 C++ 代码,就像在 C++ 中发送垃圾邮件

new
一样,表明你是一名 Java 开发人员正在用 C++ 编写 Java 代码。为确保在回答这个问题时,我快速浏览了一些信誉良好的库,但没有找到一个非绝对必要的实例。

但是,如果某些数据结构确实需要这种东西,对于较低级别的数据结构可能就是这种情况,最惯用的方法可能是使用

unsafe{}
,只需使用原始指针。例如,这是在标准库中处理文件句柄的方式。但是,如果您仍在学习 Rust,特别是如果您将其作为练习进行,那么尝试重构您的代码以消除 RAII 副作用可能是更明智的做法,因为这些通常不受欢迎,尤其是对于高-级别代码。

没有真正安全的方法是有原因的。 Rust 安全不仅仅是运行时的安全,根据类型系统,即使是 std::process::exit 也应该是安全的,即使在真正惯用的 C++ 代码中也会导致内存泄漏。


以足够的空间回答评论。为脚本语言实现调用堆栈可以像下面这样简单:

在这里,我假设你有一个词法分析器迭代器,它迭代枚举中定义的标记,并且你使用类似的枚举模式来获得可能的结果。

fn parse(lexer: &mut Lexer) -> Option<Expression> {
    let mut head = {
        let token = lexer.next();
        match token.kind {
            TokenKind::Foo => {
                ...
            }
            TokenKind::Bar => {
                ...
            }
            TokenKind::Call => {
                ...
                return parse(lexer)
            }
            TokenKind::Ret => {
                return None
            }
        }
    return Some(head)
    }
}

或者,您总是通过模拟调用堆栈来重构任何递归函数。这是一个递归反转二叉树的简单示例,但是通过模拟调用堆栈:(这是从 https://github.com/tsoding/nonrec-invert-binary-tree-rust 和我你知道你可以比这更容易倒树。这只是为了展示模式。)

use std::fmt::{Display, Debug};

type NodeRef<T> = Option<Box<Node<T>>>;

#[derive(Debug, Default, Clone)]
struct Node<T> {
    value: T,
    left: NodeRef<T>,
    right: NodeRef<T>,
}

#[derive(Debug)]
enum Action<T, U> {
    Call(T),
    Handle(U)
}

fn invert_tree<T: Clone + Debug>(root: &NodeRef<T>) -> NodeRef<T> {
    let mut arg_stack = Vec::<Action<&NodeRef<T>, &T>>::new();
    let mut ret_stack = Vec::<NodeRef<T>>::new();

    use Action::*;
    arg_stack.push(Call(root));
    while let Some(action) = arg_stack.pop() {
        match action {
            Call(root) => if let Some(node) = root {
                arg_stack.push(Handle(&node.value));
                arg_stack.push(Call(&node.right));
                arg_stack.push(Call(&node.left));
            } else {
                ret_stack.push(None)
            },
            Handle(value) => {
                let left = ret_stack.pop().unwrap();
                let right = ret_stack.pop().unwrap();
                ret_stack.push(Some(Box::new(Node{value: value.clone(), left, right})));
            },
        }
    }

    ret_stack.pop().unwrap()
}

1
投票

是的,

Rc<RefCell>
将是一个可行的解决方案。尽管我会质疑 pop-on-drop 的整个架构。例如,这里应该发生什么?

// Create our table for testing
let table = &mut SymbolTable::new();
{
    // Enter a new scope of code, pushing a new stack frame
    let _stacker1 = SymbolStacker::new(table);
    {
        // Enter another scope
        let _stacker2 = SymbolStacker::new(table);

        // Drop the first stacker. This is valid use of your API.
        drop(_stacker1);

        // stacker2 still exists. What should the state of the table be
        // now? In your implementation, dropping `stacker1` would have
        // dropped the frame of `stacker2`. Is that desired behavior?
        // If not, the Rust-y way would be to prevent this usage on API level.
    }
}

这是对您的 API 的有效使用。在 Rust 中,一种范例是 API 应该只能以正确的方式使用。

当然有办法做到这一点。我不知道你的整个用例,但这样的事情对我来说很有意义并且编译得很好:

/// Symbol table example to for a hypothetical script interpreter
/// Simplified to demonstrate issues with borrowing references
use std::collections::HashMap;

/// For our purposes a frame from is just a string->string map
type SymbolFrame = HashMap<String, String>;

/// Our stack of frames is just a vector of stack frame
/// Not performant, but that's not the point
type SymbolFrameStack = Vec<SymbolFrame>;

/// The SymbolTable type contains a stack of symbol frames
/// and routines for working with them
pub struct SymbolTable {
    frames: SymbolFrameStack,
}
impl SymbolTable {
    /// Start with an initial stack with one frame
    pub fn new() -> Self {
        SymbolTable {
            frames: vec![SymbolFrame::new()],
        }
    }

    /// Push and pop frames
    pub fn push(&mut self) {
        self.frames.push(SymbolFrame::new());
    }
    pub fn pop(&mut self) {
        self.frames.pop();
    }

    /// See if a symbol exists by name anywhere in the frames
    pub fn contains(&self, name: &str) -> bool {
        for frame in self.frames.iter() {
            if frame.contains_key(name) {
                return true;
            }
        }
        return false;
    }

    pub fn insert(&mut self, name: String, value: String) {
        self.frames.last_mut().unwrap().insert(name, value);
    }
}

/// Push a frame on new(); pop the frame on drop()
pub struct SymbolStacker<'a> {
    table: &'a mut SymbolTable,
}
impl SymbolStacker<'_> {
    pub fn new(table: &mut SymbolTable) -> SymbolStacker {
        table.push();
        SymbolStacker { table }
    }

    pub fn get_table(&mut self) -> &mut SymbolTable {
        self.table
    }

    pub fn new_frame(&mut self) -> SymbolStacker {
        SymbolStacker::new(self.table)
    }
}
impl<'a> Drop for SymbolStacker<'a> {
    fn drop(&mut self) {
        self.table.pop();
    }
}

#[test]
fn bad_test_symbol_table() {
    // Create our table for testing
    let table = &mut SymbolTable::new();
    {
        let mut stacker = SymbolStacker::new(table);
        stacker
            .get_table()
            .insert("foo".to_string(), "abc".to_string());

        assert!(stacker.get_table().contains("foo"));
        assert!(!stacker.get_table().contains("bar"));

        // Enter a new scope of code, pushing a new stack frame
        {
            let mut stacker = stacker.new_frame();

            stacker
                .get_table()
                .insert("bar".to_string(), "42".to_string());

            assert!(stacker.get_table().contains("foo"));
            assert!(stacker.get_table().contains("bar"));
        }

        assert!(stacker.get_table().contains("foo"));
        assert!(!stacker.get_table().contains("bar"));
    }
}

注意下面的代码现在是编译错误:

#[test]
fn bad_test_symbol_table() {
    // Create our table for testing
    let table = &mut SymbolTable::new();
    {
        let mut stacker = SymbolStacker::new(table);
        stacker
            .get_table()
            .insert("foo".to_string(), "abc".to_string());

        assert!(stacker.get_table().contains("foo"));
        assert!(!stacker.get_table().contains("bar"));

        // Enter a new scope of code, pushing a new stack frame
        {
            let mut stacker2 = stacker.new_frame();

            stacker2
                .get_table()
                .insert("bar".to_string(), "42".to_string());

            // Drop outer stacker
            drop(stacker);

            assert!(stacker2.get_table().contains("foo"));
            assert!(stacker2.get_table().contains("bar"));
        }
    }
}
error[E0505]: cannot move out of `stacker` because it is borrowed
  --> src/lib.rs:94:18
   |
87 |             let mut stacker2 = stacker.new_frame();
   |                                ------------------- borrow of `stacker` occurs here
...
94 |             drop(stacker);
   |                  ^^^^^^^ move out of `stacker` occurs here
95 |
96 |             assert!(stacker2.get_table().contains("foo"));
   |                     -------------------- borrow later used here

当然,您的用例可能更复杂,随身携带

&mut
参考可能不是正确的选择。但我认为这需要在您的问题中提供更多背景信息。 Rust 是一种非常严格的语言,许多问题需要非常具体的解决方案。

© www.soinside.com 2019 - 2024. All rights reserved.