具有可变引用成员的不可变结构体

4

我的理解是否正确:在Rust中,当引用结构体的成员时,不可能保护它们不被修改而又让引用目标值可变?(没有运行时借用检查的情况下)例如:

struct MyData<'a> {
    pub some_ref: &'a mut i32,
}

fn doit<'a>(data: &mut MyData<'a>, other_ref: &'a mut i32) {
    // I want to be able to do the following here:
    *data.some_ref = 22;
    // but make it impossible to do the following:
    data.some_ref = other_ref;
}

在某些FFI情况下,不能更改引用值可能是有用的。FFI和性能要求的原因阻止了在此处使用运行时借用检查。

在C++中,可以这样表达:

struct MyData {
    int* const some_ref;
};

void doit(const MyData &data, int* other_ref) {
    // this is allowed:
    *data.some_ref = 22;
    // this is not:
    data.some_ref = other_ref; // compile error
}

你只通过 FFI 改变引用吗?那初始化呢? - Chayim Friedman
想象一下一个程序,其中主要部分分配了MyData结构体,然后加载了两个动态库。它将这两个动态库的指针给予已分配的MyData,并允许库访问和更改由some_ref字段指向的值(它确保它们不会同时进行更改),但必须防止它们更改some_ref引用本身,因为出于自己的原因,它必须确保数据位于分配它的特定地址上。我敢打赌,你可以想出一个不涉及dylibs的例子。 - Lev Himmelfarb
如果您想防止意外滥用,Rust 可以提供保护(考虑到不安全的代码、rustc 错误、/proc/mem 等)。但如果您想防止有意的滥用,这是 Rust 无法提供的。 - Chayim Friedman
所有的误用。包括意外情况。 - Lev Himmelfarb
那么你的 C++ 代码就不正确。代码可以强制转换掉 const 并分配给引用。 - Chayim Friedman
假设出现意外情况。C++示例展示了我想要实现的内容——如果尝试更改指针而不是目标值,则会在编译时出现错误。 - Lev Himmelfarb
2个回答

1

您可以创建一个包装器类型来封装引用。如果构造函数是私有的,而且包装的引用字段也是私有的,那么您无法替换引用本身。然后,您可以实现DerefMut以允许更改引用对象。

pub struct ImmRef<'a> {
    inner: &'a mut i32,
}

impl<'a> ImmRef<'a> {
    fn new(inner: &'a mut i32) -> Self { Self { inner } }
}

impl std::ops::Deref for ImmRef<'_> {
    type Target = i32;
    fn deref(&self) -> &Self::Target { &*self.inner }
}
impl std::ops::DerefMut for ImmRef<'_> {
    fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.inner }
}

struct MyData<'a> {
    pub some_ref: ImmRef<'a>,
}

fn doit<'a>(data: &mut MyData<'a>, other_ref: &'a mut i32) {
    // I want to be able to do the following here:
    *data.some_ref = 22;
    // but make it impossible to do the following:
    // data.some_ref = other_ref;
}

您可以为FFI目的标记新类型#[repr(transparent)]

但请注意,如果代码中有一些可用的ImmRef<'a>,它可以使用诸如std::mem::replace()之类的工具来替换引用。


是的,这就是我一直在寻找的解决方案。唯一可能存在的问题是,如果某种情况下您获得了一个独立的 ImmRef 值 - 您仍然可以将其分配到 MyData :: some_ref 字段中。但是,如果您只将 ImmRef 视为其他结构体的成员(因此您无法从另一个结构体中移出并且您没有为其实现 Copy),则这不是问题。谢谢! - Lev Himmelfarb
@LevHimmelfarb 如果您有两个可变的 MyData 实例,您可以交换它们的引用。 - Chayim Friedman

0

Rust 不允许您像在 C++ 中使用 const 一样指定单个字段的可变性。相反,您应该通过将其设置为私有并仅允许通过您指定的方法进行修改来简单地 封装 数据:

struct MyData<'a> {
    some_ref: &'a mut i32,
}

impl MyData<'_> {
    pub fn set_ref(&mut self, other: i32) {
        *self.some_ref = other;
    }
}

这样,字段some_ref就不能直接被修改(在模块外部),必须使用可用的方法。


这可能是一个解决方案,尽管有点笨重。在这种特定情况下,创建DerefMut的包装实现并隐藏构造包装实例的方式对客户端代码更有效。 - Lev Himmelfarb

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