在 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 中做这种事情的建议。
我知道这不是你要找的答案,但我认为这个问题的最佳答案是“在 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()
}
是的,
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 是一种非常严格的语言,许多问题需要非常具体的解决方案。