Понимание мутации структурного поля
Из книги ржавчины о том, как мутировать области структуры:
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/С++.