为什么对已丢弃对象的可变引用仍然被视为可变引用?

3

以下是一个简单的例子:

struct Connection {}

impl Connection {
    fn transaction(&mut self) -> Transaction {
        Transaction { conn: self }
    }
}

struct Transaction<'conn> {
    conn: &'conn Connection,
}

impl<'conn> Transaction<'conn> {
    fn commit(mut self) {}
}

fn main() {
    let mut db_conn = Connection {};

    let mut trans = db_conn.transaction(); //1
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction(); //2
        }
    }
}

编译器在12处报告了两个可变借用,但事实并非如此。由于trans.commit()self按值传递,因此trans被删除,所以在2点之后不应该存在可变引用。
  1. 为什么编译器无法看到2处没有可变引用?
  2. 如何修复代码,保留相同的逻辑?

3
最终,你的代码应该只需正常工作,但 Rust 目前还未达到该目标。 - Francis Gagné
2个回答

2
你的原始代码在启用非词法生命周期时可以正常工作:
#![feature(nll)]

struct Connection {}

impl Connection {
    fn transaction(&mut self) -> Transaction {
        Transaction { conn: self }
    }
}

struct Transaction<'conn> {
    conn: &'conn Connection,
}

impl<'conn> Transaction<'conn> {
    fn commit(self) {}
}

fn main() {
    let mut db_conn = Connection {};

    let mut trans = db_conn.transaction();
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction();
        }
    }
}

非词法生命周期提高了借用检查器的准确性。编译器变得更加智能,现在可以证明更多的程序是内存安全的。

2

这里有一个可变引用。

如果你把 transaction 更改为这样:

fn transaction(&mut self) -> Transaction {
    let _: () = self;
    Transaction{conn: self}
}

您会发现编译器报错:

 = note: expected type `()`
 = note:    found type `&mut Connection`

所以self的类型是可变引用&mut Connection。然后你将它传递给从此函数返回的Transaction实例。

这意味着你的可变借用存在于trans的生命周期中(我添加了花括号以显示借用的作用域):

let mut trans = db_conn.transaction();
{ // <-------------------- Borrow starts here
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction();// <--- ####### D'oh! Still mutably borrowed
        }
    }
} // <-------------------- Borrow ends here

如果你正在寻找这种parent-><-child的设置方式,我认为你需要使用Rc<RefCell>
具体来说,需要一个Rc来引用连接传递的次数,以及使用RefCell在运行时跟踪借用而不是编译时。是的,这意味着如果你在运行时尝试两次可变地借用它,程序会报错。不知道你的架构更多信息,很难说这是否合适。
这是我的解决方案
use std::cell::RefCell;
use std::rc::Rc;

struct Connection {}

impl Connection {
    fn do_something_mutable(&mut self) {
        println!("Did something mutable");
    }
}

type Conn = Rc<RefCell<Connection>>;

struct Transaction {
    conn: Conn,
}

impl Transaction {
    fn new(connection: Conn) -> Transaction {
        Transaction { conn: connection }
    }

    fn commit(mut self) {
        self.conn.borrow_mut().do_something_mutable();
    }
}

fn main() {
    let db_conn = Rc::new(RefCell::new(Connection {}));

    let mut trans = Transaction::new(db_conn.clone());
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = Transaction::new(db_conn.clone());
            break; // Used to stop the loop crashing the playground
        }
    }
}

你没有理解我的意思。在 trans.commit(); 之后,trans 就不存在了。因此,没有可变引用。 - user1244932
您的解决方案增加了额外的内存分配,以及额外的计数器。是否可能在不需要这些不必要的东西的情况下实现相同的目标? - user1244932
感谢您的回答。 - user1244932
@user1244932 "所以在运行时不存在可变引用" - 确实如此。尽管在编译时,借用检查器无法静态地知道这一点。"没有这些不必要的东西,是否有可能达到相同的目标?" - 我相信是可能的,但需要重新构建代码,使其不通过引用来维护父-><-子关系。今晚我会抽出更多时间向你展示我的想法。 - Simon Whitehead
1
@user1244932:有希望在某个时候,Rust会获得非词法借用;也就是说,借用不再由词法作用域(从变量引入到闭合})跟踪,而是基于变量的生命周期进行跟踪。虽然我认为现在没有人在研究它。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接