< 返回版块

tottiandlsg 发表于 2023-02-02 16:01

程序如下:

use std::cell::RefCell;

#[derive(Debug)]
struct Data {
    id: usize,
    val: [u32; 10],
} 

fn main() {

    let mut data = Data {
        id: 0,
        val: [0; 10],
    };

    let mut p_data = &mut data;

    p_data.val[p_data.id] = 10;

    println!("{:?}", data);

    let rdata = RefCell::new(Data {
        id: 0,
        val: [0; 10],
    });

    let mut p_rdata = rdata.borrow_mut();

    p_rdata.val[p_rdata.id] = 10;

    println!("{:?}", rdata);
}

我想请教一下,为什么使用Refcell包裹以后,系统会报错:

196 |     p_rdata.val[p_rdata.id] = 10;
    |     ------------^^^^^^^----
    |     |           |
    |     |           immutable borrow occurs here
    |     mutable borrow occurs here
    |     mutable borrow later used here
    |

不使用Refcell的话,可以正常编译运行。 本人rust小白,感谢大家的帮助

评论区

写评论
苦瓜小仔 2023-02-06 17:18

大家都给的最简单的办法:

let index = p_rdata.id;
p_rdata.val[index] = 10;

嗯,这也是推荐做法。代码逻辑更清晰,意图更明显。

为什么不能直接 p_rdata.val[p_rdata.id] = 10

主要是因为 Deref / DerefMut 带来的间接性,这是 MIR 中关键的部分:

let mut _4: std::cell::RefMut<'_, Data>;

_7 = &mut _4; // _7: &'1 mut RefMut<'_, Data>
_6 = <RefMut<'_, Data> as DerefMut>::deref_mut(move _7)  // _6: &'1 mut Data,注意 &'1 mut RefMut<'_, Data> 被移入函数,它的生命周期与 _6 相同

_10 = &_4; // _10: &'2 RefMut<'_, Data>
_9 = <RefMut<'_, Data> as Deref>::deref(move _10) // _9: &'2 Data

_8 = ((*_9).0: usize); // 通过 &'2 Data 拿到 _8: usize
((*_6).1: [u32; 10])[_8] = const 10_u32; // 通过 &'1 mut Data 拿到 &mut [u32; 10],&'1 mut RefMut<'_, Data> 的生命周期也被延长至此,与 &'2 RefMut<'_, Data> 产生交叉,借用检查不通过

嗯,它为什么不能是两阶段借用呢?两阶段借用的 MIR 例子 有这样这样的,里面的细节真的很微妙,真的不值得 Rust 的使用者去仔细研究。

有没有其他做法呢

经验的做法是基于 slice,因为它的 Index / IndexMut 是编译器内置的操作

playground

let mut p_rdata = rdata.borrow_mut();
let data = &mut *p_rdata;
data.val[data.id] = 10;

// 或者
let data = &mut *rdata.borrow_mut();
data.val[data.id] = 20;

其 MIR 关键部分:

_8 = &mut _4; // _8: &'1 mut RefMut<'_, Data>
_7 = <RefMut<'_, Data> as DerefMut>::deref_mut(move _8) // _7: &'1 mut Data

_6 = &mut (*_7); // _6: &'2 mut Data, 这里是 reborrow
_9 = ((*_6).0: usize); // 通过 &'2 mut Data 拿到 _9: usize

((*_6).1: [u32; 10])[_9] = const 10_u32; // 通过 &'2 mut Data 拿到 &mut [u32; 10] 并修改值

想要弄懂这些细节真的很麻烦(我也不保证上面的注释没问题),要知道这里的两种语法糖(Deref(Mut) 和 IndexMut),还要拆解借用。这几乎等价于下面的操作:

let data = &mut *rdata.borrow_mut();
let v = &mut (*data).val;           
let id = (*data).id;                
v[id] = 10;                         

重点是什么(初学者忠告)

  1. 首先理解编辑错误,找到解决错误的方式(比如针对这里遇到的生命周期错误,怎么让引用不交叉 —— 单独赋值)
  2. 相比于编译器给你的生命周期错误,你更应该提醒自己注意 RefMut 的 drop,playground,那可是运行时错误 :)
aj3n 2023-02-06 15:28

仔细看了下发现这个问题还是挺有意思的,之前的回答都没办法解释为什么&mut DataRefMut<Data>P.val[P.id] = 10这个表达式上的差异.

先看看为啥&mut T能够支持P.val[P.id] = 10的写法,直观上看同一行出现了对P的可变和不可变引用,但是实际上可变引用和不可变引用发生在Data的不同字段上,rust通过splitting borrows机制放行了这种代码,可以参考这个链接https://doc.rust-lang.org/nomicon/borrow-splitting.html;

猜测RefMut<T>不支持P.val[P.id]是因为编译器没有办法对RefMut应用splitting borrows机制,除了显式拆分不可变引用(let id = P.id),应该也可以直接用可变引用的方式让编译器开心: let p_rdata = &mut *p_rdata;

aj3n 2023-02-06 11:15

看错报错行了,我提出的问题只有在这个修复这个报错以后运行时才能体现; 其他人已经提到过了把对p_rdata.id单独成行拷出来; 我自己的经验是同一行某个变量的可变和不可变引用是可以同时出现的,只要在执行流程上两者不会同时存在就好,比如:

let mut a = vec![0];
a.drain(..a.len());

第二行就同时出现了a的可变和不可变引用。 不过参数比较多的时候判断起来比较麻烦,一般直接写出来,编译报错改了就好(面向编译器编程🤦).

--
👇
tottiandlsg: 非常感谢您的回复! 我可不可以这样理解,原则是不允许同时操作同一个可变引用的,但是对于结构体的不同成员,其内存地址是不一样的,编译器可以推断出其不会造成内存安全,所以不会报错。但是使用RefCell进行包裹后,编译器会将其整体看待,认为是在同时操作同一内存,所以会报错。

--
👇
aj3n: 因为原始的引用类型(&T/&mut T)的失效编译器能帮你推断出来, 但是RefCell的引用类型(Ref/RefMut)则没有特殊对待, 需要手动在p_rdata.val[p_rdata.id] = 10;之后加一句drop(p_rdata); 可以了解下关键词non-lexical lifetimes, drop scope;

作者 tottiandlsg 2023-02-03 09:54

非常感谢您的回复! 我可不可以这样理解,原则是不允许同时操作同一个可变引用的,但是对于结构体的不同成员,其内存地址是不一样的,编译器可以推断出其不会造成内存安全,所以不会报错。但是使用RefCell进行包裹后,编译器会将其整体看待,认为是在同时操作同一内存,所以会报错。

--
👇
aj3n: 因为原始的引用类型(&T/&mut T)的失效编译器能帮你推断出来, 但是RefCell的引用类型(Ref/RefMut)则没有特殊对待, 需要手动在p_rdata.val[p_rdata.id] = 10;之后加一句drop(p_rdata); 可以了解下关键词non-lexical lifetimes, drop scope;

lithbitren 2023-02-03 01:42

一般来说的经验就是borrow_mut等可变引用不能同时出现在同一行。

既然id可以copy,那先把id给copy出来再运算就可以了。

不过在使用borrow_mut的时候最好用作用域包起来或者用完就drop掉,免得后面再用麻烦,比如这个例子,不drop掉,打印都打印不出来,只能打印出一个borrow的状态。

use std::cell::RefCell;

#[derive(Debug)] 
struct Data { id: usize, val: [u32; 10], }

fn main() {

    let mut data = Data {
        id: 0,
        val: [0; 10],
    };
    
    let mut p_data = &mut data;
    
    p_data.val[p_data.id] = 10;
    
    println!("{:?}", data);
    
    let rdata = RefCell::new(Data {
        id: 0,
        val: [0; 10],
    });
    
    let mut p_rdata = rdata.borrow_mut();
    
    let id = p_rdata.id;
    
    p_rdata.val[id] = 10;

    // drop(p_rdata);
    
    println!("{:?}", rdata);
}

zzhaolei 2023-02-02 18:15

不知道这么理解的对不对。

p_rdata.val[p_rdata.id],p_rdata已经被p_rdata.val(deref_mut)借用为&mut self了,然后p_rdata.id(deref)借用不可变引用,然后报错了。

可以这么改,这会先通过(*p_rdata.deref()).id获取到id,然后再使用。id字段实现了copy。

let index: usize = p_rdata.id;
p_rdata.val[index] = 10;
aj3n 2023-02-02 16:45

因为原始的引用类型(&T/&mut T)的失效编译器能帮你推断出来, 但是RefCell的引用类型(Ref/RefMut)则没有特殊对待, 需要手动在p_rdata.val[p_rdata.id] = 10;之后加一句drop(p_rdata); 可以了解下关键词non-lexical lifetimes, drop scope;

1 共 7 条评论, 1 页