如何在不破坏封装的情况下返回 RefCell 中某个东西的引用?

60

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

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 | |     }
   | |_____^

问题在于我不能在 Foo 上拥有返回借用的 vec 的函数,因为借用的 vec 仅在 Ref 的生命周期内有效,但是 Ref 立即超出了作用域。
我认为 Ref 必须保持不变 因为

RefCell<T> 使用 Rust 的生命周期来实现“动态借用”,这是一个过程,通过该过程可以临时、独占、可变地访问内部值。对于 RefCell<T> 的借用是在运行时跟踪的,而 Rust 的本机引用类型完全是在编译时静态跟踪的。由于 RefCell<T> 借用是动态的,因此可能会尝试借用已经被可变借用的值;当发生这种情况时,它会导致任务恐慌。

现在我可以编写一个像这样返回整个内部的函数:
pub fn get_mutable_interior(&self) -> std::cell::Ref<MutableInterior>;

然而,这可能会将实际上是私有实现细节的字段(例如此示例中的MutableInterior.hide_me)暴露给Foo。理想情况下,我只想公开vec本身,可能带有一个保护来实现动态借用行为。然后调用者就不必了解hide_me
5个回答

47

而不是创建全新的类型,你可以使用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)
    }
}

你还可以使用像impl Trait这样的新特性来隐藏API中的Ref

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)
    }
}

如果您正在实现std::ops::Index<>特质,需要返回&Self::Output,那么使用get_item的替代方案是什么?据我所知,返回std::cell::Ref无法满足特质要求。有没有一种方法可以在该特质中进行内部可变性操作? - DanielV
实际上,我找到了一种使用UnsafeCell的方法,所以我认为这可能已经足够好了。 - DanielV
1
@DanielV 实现Index trait以返回非引用值。我不会信任UnsafeCell的实现,因为它很可能会引入内存不安全性。 - Shepmaster
注意:Mutex 没有 map,这意味着这种情况只能单线程(从 cell.rsmutex.rs 中的代码来看,我认为至少需要重新设计 MutexGuard 以支持此类操作)。我认为在这种情况下可以使用 @Levan 下面的一种变体解决方案,但我还没有尝试过。 - Kevin Anderson

34

你可以创建一个类似于Ref<'a,T>守卫的新结构体,以包装这个Ref并避免它超出范围:

use std::cell::Ref;

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

然后,您可以为其实现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
这是唯一的/惯用的方法吗?看起来有点麻烦... 不过我想,你可以直接借用块中的内部内容,而不是使用getItems()方法,然后它会超出作用域(或者其他什么...)。 - norcalli
3
针对 RefCell 的特殊情况,当引用超出作用域时需要通知对象(这就是 Ref 析构函数的作用)。在这里,我们需要保留这种行为(OP 的错误是由于 Ref 实例被过早地丢弃导致的),因此需要进行封装。 - Levans

5
你可以使用 Rc 来包装 Vec
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();
}

当您需要更改 Vec 时,请使用 Rc::make_mut 来获取对 Vec 的可变引用。如果仍然有其他 Rc 引用到 Vec,则 make_mut 将使该 Rc 脱离其他 Rc,克隆 Vec 并将其更新为指向新的 Vec,然后给您一个可变引用。这确保了其他 Rc 中的值不会突然更改(因为 Rc 本身不提供内部可变性)。

1

Ref<X>是通常的答案。 然而,有时候,比如设计一个trait时,需要一个一致的接口。例如一个带有:

fn get_x(&self) -> &X

将会防止在 RefCell<X> 中存储 x 的结构体,而:

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

这将防止使用结构体将 x 存储为普通的 X

可以使用一个模式来解决这个问题,即采用操作数据的方法。

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

适用于同时使用 XRefCell<X>(或任何其他组合)的类的编程代码:

fun(&self.x);
// or
fun(&self.x.borrow());

1

如果你真的必须返回对数据的引用(如外部 crate 需要某种接口或类似情况),你可以使用Rc::leak进行操作,目前这需要使用nightly版,直到它稳定下来,你可以使用下面这个小函数:

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

对于原始的“泄漏”同样适用以下免责声明:
底层的RefCell再也无法进行可变借用,它将一直被认为是已经被不可变借用的。泄漏超过一个常量数量的引用是不明智的。如果总共发生的泄漏数量较小,则可以再次进行不可变借用。

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