如何在Rust中强制一个结构体的字段始终不可变?

34
在Rust中,您不需要在struct内指定可变性,而是从变量绑定中继承。这很好,但是是否有可能强制一个字段始终不可变,即使根可变?例如,这种假设语法:
struct A {
    immut s: Shape, // immutable by design
    bla: Bla, // this field inheriting (im)mutability
}
let mut a = make_a();
a.s = x/*...*/; // illegal

这将有助于保持程序中良好的语义限制,就像Java中的final所做的那样(在非常有限的情况下)。

此外,我们可以想象这种类型的struct具有对内部不可变数据的一些非所有权引用,从而利用这种不可变性...


如果你将它设为priv并且不在同一模块内的任何代码中修改它,那么它就是有效的不可变对象。当然,你也可以整体替换a(a = make_another_a();),这可能会带来问题,也可能不会。 - user395760
6个回答

27
无法使单个字段不可变。这是在Rust的一个古老版本(0.8之前)中提供的选项,但由于规则混淆了很多人而被删除。你可能会问为什么它会混淆,就像这样考虑:如果字段声明为可变,结构体声明为可变,并且使用的引用是不可变引用(&),那么该字段就是_______
最好的方法,如Lily Ballard所指出的那样,是将Shape字段声明为私有,并使用impl A {...}创建一个getter方法。
mod inner {
    pub struct A {
        s: i32, // can't be seen outside of module
        pub bla: i32,
    }

    impl A {
        pub fn new() -> Self {
            Self { s: 0, bla: 42 }
        }

        pub fn get_s(&self) -> i32 {
            self.s
        }
    }
}

let mut a = inner::A::new();
a.s = 42; // illegal
println!("{}", a.s); // also illegal
println!("{}", a.get_s()); // could be made to serve as a read-only method

error[E0616]: field `s` of struct `main::inner::A` is private
  --> src/main.rs:20:5
   |
20 |     a.s = 42; // illegal
   |     ^^^

error[E0616]: field `s` of struct `main::inner::A` is private
  --> src/main.rs:21:20
   |
21 |     println!("{}", a.s); // also illegal
   |                    ^^^

有一个命题可能会完全放弃可变性和不可变性的概念(你不能说一个结构体永远不会改变)。请参见Niko's explanation以了解这种变化。


好的,我明白这可能会让人感到困惑。出于好奇,当获取一个&A并尝试改变其字段时,实际行为是什么?我的猜测是这是不可能的,因为不可变性是深层次的(继承的)... - Lionel Parreaux
1
这里有一份由kibwen提供的详细解释:http://www.reddit.com/r/rust/comments/264d2t/history_question_why_was_mut_from_structsclasses/ - Daniel Fath
@LP_ 基本上,您继承了结构体字段的可变性。例如,struct Foo {priv mut stuff} 可以被修改,因此例如 let a = &Foo { stuff: 2 } 可以使用 a.stuff = 3 进行修改。您永远无法保证字段是不可变的,那又怎么可能呢? - Daniel Fath

18

你可以创建一个结构体,并仅为其实现Deref trait。没有DerefMut trait,其中包含的值将无法被修改。

https://doc.rust-lang.org/std/ops/trait.Deref.html

这样编译器会使成员可用,就好像它没有封装在另一个结构体中一样,无需编写getter方法调用。

use std::ops::Deref;

/// A container for values that can only be deref'd immutably.
struct Immutable<T> {
    value: T,
}

impl<T> Immutable<T> {
    pub fn new(value: T) -> Self {
        Immutable { value }
    }
}

impl<T> Deref for Immutable<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

struct Foo {
    bar: Immutable<Vec<u8>>,
    baz: usize,
}

impl Foo {
    pub fn new(vec: Vec<u8>) -> Self {
        Foo {
            bar: Immutable::new(vec),
            baz: 1337,
        }
    }

    pub fn mutate(&mut self) {
        self.bar.push(0); // This will cause a compiler error
    }
}

|
|         self.bar.push(0);
|         ^^^^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `runnable::immutable::Immutable<std::vec::Vec<u8>>`

2
如果您最终采用了这种方法,请记得为 Immutable<T> 实现与 T 相同的特性,否则您库的用户在试图将 Immutable<T> 用作 T 时可能会遇到奇怪的错误。请参见此示例 - Filipe Rodrigues
你可以随时使用解引用来修复这个问题:示例。但如果你想要普通特征使用的人机工程学,那么你可能会发现在 Immutable<T> 上实现一个或两个特征是可取的。 - danthedaniel
1
这并不能阻止你将一个新值放入 bar 中。 - Alex
公正的观点 - 这实际上是OP所要求的。我想,如果您将私有字段与getter结合使用,并使用此不可变包装器结构,则可以获得完美的、深度不可变的解决方案(在结构体外部使用时)。 - danthedaniel

4

您不能强制使字段不可变。当需要时,结构体如何改变其自身的值?

您可以将字段设置为私有,然后公开 getter 方法以返回对其的引用(或复制/克隆该值)。


26
能够改变数据并不总是有意义的;例如,您听说过函数式编程吗? Java中的final修饰符可以被认为是无用的,因为一个简单的getter方法可以实现相同的效果。这只是让它变得更容易(让人们更自然地做到这一点--在代码中避免无用的潜在状态总是好的)和更清晰(自动记录值永远不会改变可能会很有用)。 - Lionel Parreaux
@LP_ 我认为决定是否要进行改变应该由结构体的API用户来决定...如果他们不想改变,他们可以持有一个不可变的引用。 - David Callanan
3
这取决于结构体是否必须维护其字段的不变式,这些不变式未受类型系统强制执行。例如,Vec有一个私有字段len和一个公共getterfn len(&self)。改变len字段将破坏Vec的安全性,因此必须保持只读。 - Lily Ballard
@LilyBallard 抱歉,我在谈论只有公共字段的结构体。 - David Callanan
不变性是一种非常有价值的结构。作为一个经验法则,我认为默认情况下应该使所有东西都是不可变的。可变性应该是例外。 - Alkis Mavridis

4

今天实现这个功能最简单的方法可能是使用来自著名开发者David Tolnay(serde等作者)的readonly crate。从GitHub文档中可以了解到:

该crate提供了一个属性宏,用于公开结构体字段,这些字段在同一模块内可读写,但在模块外只能读取。

#[readonly::make]
pub struct S {
    // This field can be read (but not written) by super.
    #[readonly]
    pub(super) readable: i32,

    // This field can be neither read nor written by other modules.
    private: i32,
}

1

在许多情况下,一个关联常量可以实现所需的行为:

struct Foo { blah: Blah }

impl Foo {
    const S: Shape = Shape { x: 1, y: 1 };
}

当然,常量不能在创建时被设置,它们是在编译时设置的。如果需要动态性,则将字段设置为私有(如其他答案所述)将起作用。

1
一个解决方案可能是采取更一般化的方法:
pub struct Immutable<T> {
    value : T ,
}

impl<T> Immutable<T> {

    pub fn new(value : T) -> Immutable<T> {
        Immutable { value : value }
    }

    pub fn get( &self) -> &T { &self.value }
}

现在,每种类型都可以在任何情况下使用不可变的结构体。
将其放入模块中可以避免更改不可变对象的内容。仍然可以通过用新对象覆盖它来更改持有不可变对象本身的变量,但您应该注意 Immutable::new 语句,并尽可能避免使用它。

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