【问题标题】:How do I return a reference to something inside a RefCell without breaking encapsulation?如何在不破坏封装的情况下返回对 RefCell 内某些内容的引用?
【发布时间】:2023-03-25 09:13:02
【问题描述】:

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

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 必须坚持because

RefCell&lt;T&gt; 使用 Rust 的生命周期来实现“动态借用”,这是一个可以声明对内部值的临时、独占、可变访问的过程。 RefCell&lt;T&gt;s 的借用是在“运行时”跟踪的,这与 Rust 的本地引用类型不同,后者在编译时完全静态跟踪。因为RefCell&lt;T&gt; 借用是动态的,所以可以尝试借用一个已经可变借用的值;发生这种情况时会导致任务恐慌。

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

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

但是,这可能会向Foo 公开真正私有的实现细节的字段(在此示例中为MutableInterior.hide_me)。

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

【问题讨论】:

    标签: rust encapsulation contravariance mutability interior-mutability


    【解决方案1】:

    您可以创建一个类似于 RefCell::borrow() 返回的 Ref&lt;'a,T&gt; 守卫的新结构,以包装此 Ref 并避免其超出范围,如下所示:

    use std::cell::Ref;
    
    struct FooGuard<'a> {
        guard: Ref<'a, MutableInterior>,
    }
    

    然后,您可以为它实现Deref trait,这样它就可以像&amp;Vec&lt;i32&gt; 一样使用:

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

    【讨论】:

    • 这是唯一/惯用的方法吗?似乎有点麻烦......虽然我想代替 getItems() 方法,您可以直接借用块中的内部结构,然后它会超出范围(或其他东西......)
    • @Norcalli 在RefCell 的特定情况下,当引用超出范围时需要通知对象(这就是Ref 的析构函数所做的)。在这里,我们需要保留这种行为(OP的错误是由于Ref实例被过早丢弃),从而对其进行封装。
    【解决方案2】:

    您可以使用Ref::map(从Rust 1.8 开始),而不是创建一个全新的类型。这与Levans' existing answer 的结果相同:

    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 特征,而不是 get_item ,这需要你返回 &Self::Output 。据我所知,返回 std::cell::Ref 不会满足特征要求。有没有办法针对该特征进行内部可变性?
    • 其实我找到了一种使用 UnsafeCell 的方法,所以我认为这可能就足够了。
    • @DanielV Implementing Index trait to return a value that is not a reference。我不相信UnsafeCell 实现,因为它很可能会引入内存不安全。
    【解决方案3】:

    您可以将Vec 包装在Rc 中。

    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 的可变引用。如果还有其他Rcs 引用Vecmake_mut 会将Rc 与其他Rcs 分离,克隆Vec 并更新自身以引用新的Vec,然后给你一个可变的引用。这确保了其他 Rcs 中的值不会突然改变(因为 Rc 本身不提供内部可变性)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-25
      • 1970-01-01
      • 1970-01-01
      • 2018-05-09
      • 2014-09-15
      • 1970-01-01
      • 2011-03-11
      相关资源
      最近更新 更多