Понимание мутации структурного поля

Из книги ржавчины о том, как мутировать области структуры:

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

и позже:

Мутируемость является свойством связывания, а не самой структуры.

Это кажется мне интригующим, потому что point.x = 5 не выглядит так, как будто я переписываю переменную point. Есть ли способ объяснить это, чтобы он был более интуитивным?

Единственный способ, которым я могу обернуть голову, - это "представить", что я переписываю point копию оригинала point с другим значением x (даже не уверен, что это точно).

Ответы

Ответ 1

У меня была та же путаница. Для меня это произошло из двух разных недоразумений. Во-первых, я пришел с языка, где переменные (aka bindings) были неявно ссылками на значения. На этом языке было важно провести различие между мутацией ссылки и изменением значения, которое было упомянуто. Во-вторых, я подумал, что "сама структура" в книге относится к инстанцированному значению, но "структурой" это означает спецификацию/декларацию, а не конкретное значение этого типа.

Переменные в Rust отличаются. Из reference:

Переменная является компонентом фрейма стека...

Локальная переменная (или локальное распределение стека) содержит значение непосредственно, выделенных в памяти стека. Значение является частью стека кадр.

Таким образом, переменная является компонентом фрейма стека - куском памяти, который непосредственно сохраняет значение. Нет никакой ссылки, чтобы отличить от самого значения, никакой ссылки на мутацию. Переменная и значение представляют собой один и тот же кусок памяти.

Следствием этого является то, что переупорядочение переменной в смысле ее изменения для ссылки на другой кусок памяти несовместимо с моделью памяти Rust. (n.b. let x = 1; let x = 2; создает две переменные.)

Таким образом, книга указывает, что изменчивость объявляется на уровне "на кусок памяти", а не как часть определения структуры.

Единственный способ, которым я могу обернуть голову, - это "представить", что я перестановка указывает на копию исходной точки с другим x (даже не уверен, что точно)

Вместо этого представьте, что вы меняете один из 0 в куске памяти на 5; и что это значение находится в памяти, обозначенной point. Интерпретировать "привязка изменчива" означает, что вы можете мутировать кучу памяти, обозначенную привязкой, включая мутацию только ее части, например. установив поле struct. Подумайте о том, чтобы переписать переменные ржавчины так, как вы описываете, как не выраженные в Rust.

Ответ 2

Это кажется мне интригующим, потому что point.x = 5 не похоже, что я переписываю переменную точку. Есть ли способ объяснить это, чтобы он был более интуитивным?

Все это говорит о том, что независимо от того, является ли что-то изменчивым, определяется оператором let - (привязка) переменной, а не как свойство типа или какого-либо определенного поля.

В примере point и его поля изменяемы, потому что point вводится в оператор let mut (в отличие от простого оператора let), а не из-за некоторого свойства типа point в целом.

В отличие от этого, чтобы показать, почему это интересно: на других языках, таких как OCaml, вы можете пометить определенные поля, изменяемые в определении типа:

type point =
   { x: int;
     mutable y: int;
   };

Это означает, что вы можете мутировать поле y каждого значения point, но вы никогда не сможете мутировать x.

Ответ 3

Здесь "привязка" не является глаголом, это существительное. Вы можете сказать, что привязки Rust являются синонимами переменных. Поэтому вы можете прочитать этот отрывок, например

Мутируемость является свойством переменной, а не самой структуры.

Теперь, я думаю, это должно быть ясно - вы отмечаете переменную как изменяемую, и поэтому вы можете изменять ее содержимое.

Ответ 4

@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 для "clobber" существующего привязки (новый адрес стека). Это происходит, даже если переменная была объявлена ​​mut, поэтому будьте осторожны.
  • Используйте mut для повторного использования существующего адреса стека, но не используйте let при переназначении.

Этот тест показывает пару интересных вещей:

  • Если вы переназначите всю измененную структуру, это эквивалентно назначению каждого элемента по отдельности.
  • Адрес переменной, содержащей структуру, совпадает с адресом первого элемента. Я предполагаю, что это имеет смысл, если вы исходите из фона C/С++.