c++ RAII模式修改new()和drop()上的另一个对象的Rust解决方案

y4ekin9u  于 2023-02-26  发布在  其他
关注(0)|答案(2)|浏览(118)

在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 做这种事情的建议。

nzk0hqpo

nzk0hqpo1#

我知道这不是你想要的答案,但我认为这个问题的最佳答案是“在Rust中以这种方式编写代码是混乱的,因为Rust不是为这种方式设计的。”在Rust中使用RAII作用域的副作用表明您是一个尝试在Rust中编写C代码的C开发人员,就像在C中发送垃圾邮件new是在告诉您一样。We“你是一名用C编写Java代码的Java开发人员。为了确定答案,我很快就找遍了一些有信誉的库,没有找到一个不是绝对必要的示例。
然而,如果你真的需要在一些数据结构中使用这种方法,这可能是低级数据结构的情况,可能最惯用的方法是使用unsafe{},简单地使用一个原始指针。例如,这是标准库中处理文件句柄的方法。然而,如果你还在学习rust,特别是如果你把它作为一个练习,尝试重构代码以避免RAII的副作用可能更明智,因为这些副作用通常是不受欢迎的,尤其是对于高级代码。
没有真实的安全的方法来做到这一点是有原因的。Rust安全不仅仅是运行时的安全,即使是std::process::exit也应该根据类型系统是安全的,这将导致内存泄漏,即使是在真正惯用的C++代码中。

qojgxg4l

qojgxg4l2#

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

// 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"));
    }
}

请注意,下面的代码现在是一个编译错误:
一个二个一个一个
当然,您的用例可能会更复杂,并且随身携带&mut引用可能不是正确的选择,但我认为这需要您的问题有更多的上下文,Rust是一种非常严格的语言,许多问题需要非常具体的解决方案。

相关问题