理解结构体字段的变异

17

来自Rust书籍关于如何修改结构体字段的内容:

let mut point = Point { x: 0, y: 0 };
point.x = 5;

后来:

可变性是绑定的属性,而不是结构本身的属性。

对我来说,这似乎不符合直觉,因为 point.x = 5 看起来并没有重新绑定变量 point。有没有一种方法可以更直观地解释这个问题?

我唯一能理解这个问题的方法是“想象”我正在将 point 重新绑定到具有不同 x 值的原始 Point 的副本(甚至不确定是否准确)。

4个回答

11
这对我来说似乎有些违反直觉,因为 point.x = 5 看起来并不像是重新绑定变量 point。有没有一种方式能够更加直观地解释这个问题?
所有这篇文章想表达的是,一个变量是否可变取决于变量的绑定(let语句),而不是类型或任何特定字段的属性。
在这个例子中,point及其字段是可变的,因为它们是在let mut语句(而不是简单的let语句)中引入的,而不是因为Point类型具有某些特性。
与此形成对比的是,在其他语言(如OCaml)中,你可以在类型的定义中标记某些字段为可变的。
type point =
   { x: int;
     mutable y: int;
   };
意思是您可以改变每个“点”值的“y”字段,但不能改变“x”。

这意味着您可以更改每个point值的y字段,但永远不能更改x


7
在这里,“binding”不是动词,而是名词。你可以说在Rust中,绑定与变量同义。因此,您可以将该段落理解为:
“可变性是变量的属性,而不是结构本身的属性。”
现在,我想应该很清楚了-您将变量标记为可变的,因此可以修改其内容。

2
好的,我觉得这有点帮助。所以在变量前面加上 mut 就像是给变量应用了一个可变的 属性。而这个属性意味着某些事情,比如重新绑定和结构字段修改。这准确吗? - Kelvin
是的,你大部分是正确的(除了 mut 并不影响重新绑定,也就是说,即使 x 不是 mut,你仍然可以写 let x = 10; let x = 12。实际上,mut 允许的是对值进行赋值(无论是变量本身还是结构体内的字段),以及获取 &mut 引用,同样是针对值或子字段。 - Vladimir Matveev

7
我曾经也有同样的困惑。对我来说,这是源于两个不同的误解。首先,我来自一种变量(也称为绑定)隐含地引用值的语言。在那种语言中,区分突变的是引用还是被引用的值很重要。其次,我认为书中所说的“结构本身”是指实例化的值,但“结构”指的是规范/声明,而不是该类型的特定值。
Rust中的变量是不同的。根据reference
引用如下: 变量是堆栈帧的组成部分... 本地变量(或堆栈本地分配)直接保存一个值, 分配在堆栈内存中。该值是堆栈帧的一部分。
因此,变量是堆栈帧的组成部分 - 一块内存 - 直接保存值。没有引用来区分值本身,也没有引用可以改变。变量和值是相同的内存块。
一个结果是,重新绑定变量的意义在于将其更改为引用不同的内存块,这与Rust的内存模型不兼容。(n.b. let x = 1; let x = 2; 创建了两个变量。)
因此,本书指出可变性是在“每个内存块”级别声明的,而不是作为结构定义的一部分。

我能想到的唯一方法就是“想象”我正在将point重新绑定到具有不同x值的原始Point的副本中(甚至不确定是否准确)

相反,想象一下你正在将内存块中的一个0更改为5;并且该值驻留在由point指定的内存中。将“绑定是可变的”解释为您可以更改绑定指定的内存块,包括仅更改其中的一部分,例如通过设置结构字段来进行突变。将Rust变量重新绑定视为无法在Rust中表达的方式。

你让我找到了正确的方向。我需要编写一个演示来看它的实际效果(请查看我的答案)。 - Kelvin
@Kelvin 感谢您发布您的演示。这是一个很好的表达方式。 - m-n

6
@m-n的回答让我找到了正确的方向。这与堆栈地址有关!以下是一次演示,让我清晰地了解了实际发生的情况。
struct Point {
    x: i64,
    y: i64,
}

fn main() {
    {
        println!("== clobber binding");
        let a = 1;
        println!("val={} | addr={:p}", a, &a);
        // This is completely new variable, with a different stack address
        let a = 2;
        println!("val={} | addr={:p}", a, &a);
    }
    {
        println!("== reassign");
        let mut b = 1;
        println!("val={} | addr={:p}", b, &b);
        // uses same stack address
        b = 2;
        println!("val={} | addr={:p}", b, &b);
    }
    {
        println!("== Struct: clobber binding");
        let p1 = Point{ x: 1, y: 2 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        let p1 = Point{ x: 3, y: 4 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);
    }
    {
        println!("== Struct: reassign");
        let mut p1 = Point{ x: 1, y: 2 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        // each of these use the same addresses; no new addresses
        println!("   (entire struct)");
        p1 = Point{ x: 3, y: 4 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        println!("   (individual members)");
        p1.x = 5; p1.y = 6;
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);
    }
}

输出结果(每次运行时地址略有不同):

== clobber binding
val=1 | addr=0x7fff6112863c
val=2 | addr=0x7fff6112858c
== reassign
val=1 | addr=0x7fff6112847c
val=2 | addr=0x7fff6112847c
== Struct: clobber binding
xval,yval=(1, 2) | pointaddr=0x7fff611282b8, xaddr=0x7fff611282b8, yaddr=0x7fff611282c0
xval,yval=(3, 4) | pointaddr=0x7fff61128178, xaddr=0x7fff61128178, yaddr=0x7fff61128180
== Struct: reassign
xval,yval=(1, 2) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
   (entire struct)
xval,yval=(3, 4) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
   (individual members)
xval,yval=(5, 6) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0

重点如下:
  • 使用let来“破坏”一个现有的绑定(新的堆栈地址)。即使变量被声明为mut,这种情况也会发生,所以要小心。
  • 使用mut来重复使用现有的堆栈地址,但是重新分配时不要使用let

这个测试揭示了一些有趣的事情:

  • 如果重新分配整个可变结构体,则等同于逐个分配每个成员。
  • 持有结构体的变量的地址与第一个成员的地址相同。如果您来自C/C++背景,我想这是有道理的。

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