如何在不破坏封装的情况下返回对 RefCell 中某些内容的引用?

How do I return a reference to something inside a RefCell without breaking encapsulation?

提问人:Drew 提问时间:4/2/2015 最后编辑:Peter HallDrew 更新时间:8/1/2023 访问量:13465

问:

我有一个具有内部可变性的结构。

use std::cell::RefCell;

struct MutableInterior {
    hide_me: i32,
    vec: Vec<i32>,
}
struct Foo {
    //although not used in this particular snippet,
    //the motivating problem uses interior mutability
    //via RefCell.
    interior: RefCell<MutableInterior>,
}

impl Foo {
    pub fn get_items(&self) -> &Vec<i32> {
        &self.interior.borrow().vec
    }
}

fn main() {
    let f = Foo {
        interior: RefCell::new(MutableInterior {
            vec: Vec::new(),
            hide_me: 2,
        }),
    };
    let borrowed_f = &f;
    let items = borrowed_f.get_items();
}

产生错误:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:16:10
   |
16 |         &self.interior.borrow().vec
   |          ^^^^^^^^^^^^^^^^^^^^^^ temporary value does not live long enough
17 |     }
   |     - temporary value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 15:5...
  --> src/main.rs:15:5
   |
15 | /     pub fn get_items(&self) -> &Vec<i32> {
16 | |         &self.interior.borrow().vec
17 | |     }
   | |_____^

问题是我不能有一个函数来返回一个借入的,因为借用的函数仅在 的生命周期内有效,但会立即超出范围。FoovecvecRefRef

我认为必须坚持下去,因为Ref

RefCell<T>使用 Rust 的生命周期来实现“动态借用”,在这个过程中,人们可以声称对内部值的临时、排他、可变的访问。借用的 s 是在“运行时”跟踪的,不像 Rust 的原生引用类型在编译时完全静态跟踪。由于借入是动态的,因此可以尝试借入已经可变借入的值;发生这种情况时,会导致任务崩溃。RefCell<T>RefCell<T>

现在我可以编写一个这样的函数来返回整个内部:

pub fn get_mutable_interior(&self) -> std::cell::Ref<MutableInterior>;

但是,这可能会向 公开字段(在本例中)实际上是私有实现细节的字段。MutableInterior.hide_meFoo

理想情况下,我只想公开它本身,可能带有一个守卫来实现动态借用行为。这样来电者就不必了解 .vechide_me

蚀封装 内部可变性

评论


答:

34赞 Levans 4/2/2015 #1

您可以创建一个类似于 返回的防护的新结构,以便包装它并避免它超出范围,如下所示:Ref<'a,T>RefCell::borrow()Ref

use std::cell::Ref;

struct FooGuard<'a> {
    guard: Ref<'a, MutableInterior>,
}

然后,你可以为它实现 trait,这样它就可以被当作一个:Deref&Vec<i32>

use std::ops::Deref;

impl<'b> Deref for FooGuard<'b> {
    type Target = Vec<i32>;

    fn deref(&self) -> &Vec<i32> {
        &self.guard.vec
    }
}

之后,更新方法以返回实例:get_items()FooGuard

impl Foo {
    pub fn get_items(&self) -> FooGuard {
        FooGuard {
            guard: self.interior.borrow(),
        }
    }
}

并施展魔术:Deref

fn main() {
    let f = Foo {
        interior: RefCell::new(MutableInterior {
            vec: Vec::new(),
            hide_me: 2,
        }),
    };
    let borrowed_f = &f;
    let items = borrowed_f.get_items();
    let v: &Vec<i32> = &items;
}

评论

4赞 norcalli 4/2/2015
这是唯一的/惯用的方法吗?好像有点麻烦...虽然我想而不是 getItems() 方法,但您可以直接借用块中的内部结构,然后它会超出范围(或其他东西......
3赞 Levans 4/2/2015
@Norcalli 在 的特定情况下,当引用超出范围时,需要通知对象(这就是析构函数的作用)。在这里,我们需要保留这种行为(OP 的错误是由于实例被过早删除造成的),从而封装它。RefCellRefRef
47赞 Shepmaster 7/15/2018 #2

你可以使用 Ref::map(从 Rust 1.8 开始)而不是创建一个全新的类型。这与 Levans 现有的答案具有相同的结果:

use std::cell::Ref;

impl Foo {
    pub fn get_items(&self) -> Ref<'_, Vec<i32>> {
        Ref::map(self.interior.borrow(), |mi| &mi.vec)
    }
}

您还可以使用新功能,例如从 API 中隐藏:impl TraitRef

use std::cell::Ref;
use std::ops::Deref;

impl Foo {
    pub fn get_items(&self) -> impl Deref<Target = Vec<i32>> + '_ {
        Ref::map(self.interior.borrow(), |mi| &mi.vec)
    }
}

评论

0赞 DanielV 10/8/2019
如果您要实现 std::ops::Index<> 特征,而不是 get_item,这需要您返回 &Self::Output 。据我所知,返回 std::cell::Ref 不会满足特征要求。有没有办法为该特征进行内部可变性?
0赞 DanielV 10/8/2019
实际上,我找到了一种使用UnsafeCell的方法,所以我认为也许这已经足够了。
1赞 Shepmaster 10/8/2019
@DanielV 实现 Index trait 以返回非引用值。我不相信这个实现,因为它很可能会引入内存不安全。UnsafeCell
0赞 Kevin Anderson 3/29/2023
注意:不存在意味着这种东西只是单线程的(并且从两者中的代码来看,我认为至少需要重新设计以支持此类代码)。我认为在这种情况下可以使用下面 @Levan 解决方案的变体,但还没有尝试过。mapMutexcell.rsmutex.rsMutexGuard
5赞 Francis Gagné 7/15/2018 #3

您可以将 .VecRc

use std::cell::RefCell;
use std::rc::Rc;

struct MutableInterior {
    hide_me: i32,
    vec: Rc<Vec<i32>>,
}
struct Foo {
    interior: RefCell<MutableInterior>,
}

impl Foo {
    pub fn get_items(&self) -> Rc<Vec<i32>> {
        self.interior.borrow().vec.clone() // clones the Rc, not the Vec
    }
}

fn main() {
    let f = Foo {
        interior: RefCell::new(MutableInterior {
            vec: Rc::new(Vec::new()),
            hide_me: 2,
        }),
    };
    let borrowed_f = &f;
    let items = borrowed_f.get_items();
}

当您需要改变 时,请使用 Rc::make_mut 获取对 的可变引用。如果还有其他 s 引用 ,将与其他 s 分离,克隆 并更新自身以引用该新 ,然后给你一个可变的引用。这确保了其他 s 中的值不会突然更改(因为其本身不提供内部可变性)。VecVecRcVecmake_mutRcRcVecVecRcRc

1赞 c z 3/28/2023 #4

Ref<X>是通常的答案。 但是,有时,例如在设计特征时,需要一致的接口。例如,具有以下特征的特征:

fn get_x(&self) -> &X

将排除结构存储在 中,而:xRefCell<X>

fn get_x(&self) -> Ref<X>

将排除结构存储为普通 .xX

可以用来解决这个问题的模式是采用一种对数据进行操作的方法。

fn with_x(&self, fun : &dyn FnOnce(&X));

它适用于使用 and(或任何其他组合)的两个类:XRefCell<X>

fun(&self.x);
// or
fun(&self.x.borrow());
1赞 cafce25 8/1/2023 #5

如果你真的必须返回对数据的引用(因为在外部 crate 中需要一些接口或类似接口),你可以使用 Rc::leak 来做到这一点,它目前需要 ,直到它稳定下来,你可以用这个小函数来帮助自己nightly

use std::cell::Ref;
fn leak_ref<'a, T>(orig: Ref<'a, T>) -> &'a T {
    Box::leak(Box::new(orig))
}

与原文相同的免责声明适用于:leak

底层永远不会再被可变地借用,并且总是看起来已经不可变地被借用了。泄漏超过恒定数量的引用不是一个好主意。如果总共只发生了少量泄漏,则可以再次不可变地借用。RefCellRefCell