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

Из книги Rust о том, как изменять поля структуры:

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

и позже:

Изменчивость — это свойство связывания, а не самой структуры.

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

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


person Kelvin    schedule 31.07.2015    source источник


Ответы (4)


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

Переменные в Rust разные. Из справочника:

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

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

Итак, переменная — это компонент кадра стека — фрагмент памяти, — который непосредственно содержит значение. Нет ссылки на отличие от самого значения, нет ссылки на изменение. Переменная и значение — это один и тот же кусок памяти.

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

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

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

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

person m-n    schedule 01.08.2015
comment
Ты наставил меня на верный путь. Мне нужно было написать демо, чтобы увидеть его в действии (см. мой ответ). - person Kelvin; 24.08.2015
comment
@Kelvin Спасибо за публикацию вашей демонстрации. Это хороший способ выразить это. - person m-n; 25.08.2015

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

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

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

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

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

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

person fjh    schedule 31.07.2015

Здесь «привязка» — это не глагол, а существительное. Можно сказать, что в Rust привязки являются синонимами переменных. Таким образом, вы можете прочитать этот отрывок как

Изменчивость — это свойство переменной, а не самой структуры.

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

person Vladimir Matveev    schedule 31.07.2015
comment
Хорошо, думаю, это немного поможет. Таким образом, mut перед переменной просто применяет изменяемое свойство к переменной. И это свойство подразумевает определенные вещи, например. перепривязка и модификация поля структуры. Это точно? - person Kelvin; 31.07.2015
comment
Да, вы в основном правы (за исключением того, что mut не влияет на повторное связывание в том смысле, что вы все равно можете написать let x = 10; let x = 12, даже если x не является mut. Что mut фактически позволяет, так это присваивание значения (либо самой переменной, либо полю внутри нее если это структура) и беру &mut ссылок, опять же, на значение или на подполе. - person Vladimir Matveev; 01.08.2015

Ответ @ 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++.
person Kelvin    schedule 24.08.2015