Прежде чем перейти к пониманию изменяемой ссылки, давайте взглянем на очень простой пример на C.
int main() { int i = 1; i = 2; }
Этот код отлично компилируется. Давайте посмотрим на его эквивалент в Rust.
fn main() { let i:i32 = 1; i = 2; }
Это не срабатывает с ошибкой:
error[E0384]: cannot assign twice to immutable variable `i`
--> src/main.rs:3:5
|
2 | let i = 20;
| - first assignment to `i`
3 | i = 2;
| ^^^^^ cannot assign twice to immutable variable
Rust рассматривает свои переменные как неизменяемые, если не указано следующее:
let mut i:i32 = 1;
Это изменение теперь скомпилирует код.
Это самый простой способ понять красоту Rust. Он «защищает» и «охраняет» ваш код, если не указано иное. (для сильных сердцем см .unsafe
в Rust)
Я не говорю, что это невозможно в C. Как и в Rust, следующий код на C тоже не скомпилируется. Разница в том, что здесь явно упоминается const
.
int main() { const int i = 1; i = 2; }
выдаст ошибку:
main.c: In function ‘main’: main.c:3:6: error: assignment of read-only variable ‘i’ i = 2; ^
Вернемся к Rust. Изменяемая ссылка - это заимствование любого типа mut T
, позволяющее изменять T
через эту ссылку. В приведенном ниже коде показан пример изменяемой переменной с последующим изменением ее значения с помощью изменяемой ссылки ref_i
.
fn main() { let mut i:i32 = 1; let ref_i:&mut i32 = &mut i; *ref_i = 2; }
Есть предположения, что будет, если я добавлю в приведенный выше код следующую строку?
i = 3;
Да, выйдет с ошибкой:
error[E0506]: cannot assign to `i` because it is borrowed
--> src/main.rs:5:4
|
3 | let ref_i:&mut i32 = &mut i;
| - borrow of `i` occurs here
4 | *ref_i = 2;
5 | i=3;
| ^^^ assignment to borrowed `i` occurs here
Разве это не круто? Сообщение об ошибке говорит само за себя! Довольно информативно. Не надо чесать затылок.
Следующий код C (более или менее аналог вышеприведенного примера на Rust) будет компилироваться нормально.
int main() { int i = 1; int* ref_i = &i; *ref_i = 2; i = 3; }
Вы можете изменить переменную i
, даже если есть другая переменная ref_i
(указатель), указывающая на i.
Что, если мы возьмем 2 изменяемые ссылки?
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = &mut i; }
Это не сработает с ошибкой:
error[E0499]: cannot borrow `i` as mutable more than once at a time
--> src/main.rs:4:29
|
3 | let ref_i = &mut i;
| - first mutable borrow occurs here
4 | let another_ref_i = &mut i;
| ^ second mutable borrow occurs here
5 | }
| - first borrow ends here
Как видите, это не разрешает другую изменяемую ссылку, пока уже существует активная изменяемая ссылка. Обратите внимание на «активный» здесь. (см. first borrow ends here
в сообщении об ошибке).
Примечание. Даже если первое заимствование было неизменным, компилятор все равно не принял бы код.
Теперь, если я немного поверну приведенный выше код и напишу его следующим образом, компилятор Rust с радостью примет его.
fn main() { let mut i:i32 = 1; { let ref_i = &mut i; } let another_ref_i = &mut i; }
Почему? вы можете спросить. Поскольку изменяемая ссылкаref_i
заканчивается, когда заканчивается внутренняя область видимости, позволяя ссылке another_ref_i
заимствовать i
изменчиво во внешней области.
Как видите, средство проверки заимствований в Rust «всегда злится» на общую изменчивость. Есть еще один зверь, называемый висячими ссылками, который в C часто может вызывать ошибки сегментации во время выполнения. Вот где воплощения Rust спасают вас, и как это сделать! Требуется целый отдельный мегапост, чтобы охватить время жизни. А пока давайте сосредоточимся на изменяемых ссылках.
Теперь давайте посмотрим на следующий код:
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = ref_i; }
Этот компилируется. Но тогда можно спросить, почему? поскольку теперь у него есть 2 активных изменяемых ссылки ref_i
и another_ref_i
на i
. Общая изменчивость наносит ответный удар? Ну нет. Изменяемая ссылка ref_i
перемещается в another_ref_i
. Изменяемых ссылок нет n Copy
типа. Если мы добавим *ref_i = 2
в конце, компилятор выйдет из строя со следующей ошибкой:
error[E0382]: use of moved value: `ref_i`
--> src/main.rs:5:4
|
4 | let another_ref_i = ref_i;
| ------------- value moved here
5 | *ref_i = 2;
| ^^^^^^^^^^ value used here after move
|
= note: move occurs because `ref_i` has type `&mut i32`, which does not implement the `Copy` trait
Все хорошо! Следующий пример.
fn test (i:&mut i32) {} fn main() { let mut i:i32 = 1; let ref_i = &mut i; test (ref_i); *ref_i = 2; }
Этот компилируется. Но держись! Не ref_i
переместится в функцию test
, что сделает ее непригодной (т. Е. *ref_i = 2
должна выйти из строя) после вызова?
Ну не совсем. Помимо перемещения, ссылки также могут быть заимствованы заново. На месте вызова test
компилятор вставит заимствование на *ref_i
. test(ref_i)
будет переведен на test(&mut *ref_i)
. Заметано! Но подождите, поскольку*ref_i
был позаимствован кем-то другим, как может компилятор разрешить коду видоизменяться с помощью *ref_i
? Разве нельзя просто предотвратить это, сказав, что *ref_i
заимствовано? Неа! потому что на сайте вызова создается временная область видимости, которая длится только до тех пор, пока функция test
не вернется, и именно в этой области происходит повторный заимствование и происходит вместе с областью. В точке, где находится *ref_i = 2
, кроме самого ref_i
, нет никакой другой ссылки на i
.
Если подумать, если бы не было переборов, было бы невозможно использовать изменяемые ссылки с функциями, так как они всегда были бы непригодны для использования после вызова.
Теперь, когда мы знаем о переборах, будет ли компилироваться следующий пример или нет?
fn main() { let mut i:i32 = 1; let ref_i = &mut i; let another_ref_i = &mut *ref_i; //or &*ref_i *ref_i = 2; }
Нет. Причина в том, что переназначить до *ref_i
, все еще активна в области. В этом случае нет внутренней области действия.
Ошибка все объясняет! Только через another_ref_i
вы можете изменить значение.
error[E0506]: cannot assign to `*ref_i` because it is borrowed
--> src/main.rs:5:5
|
4 | let another_ref_i = &mut *ref_i; //or &*ref_i
| ------ borrow of `*ref_i` occurs here
5 | *ref_i = 2;
| ^^^^^^^^^^ assignment to borrowed `*ref_i` occurs here
Теперь, когда мы отметили несколько важных моментов, давайте взглянем на следующий код (предупреждение: мы вступаем в ошеломляющий мир вложенных ссылок)
fn main () { let mut i:i32 = 1; let j = { let x = &mut i; let y = &x; &**y }; }
Как видите, во внутренней области вид y
дважды разыменовывается, а затем заимствуется. *y
дает x
(изменяемую ссылку), которая затем снова разыменовывается, и возвращается заимствование изменяемого заимствования с разыменованием.
Теперь скомпилируем код.
error[E0597]: `x` does not live long enough
--> src/main.rs:6:16
|
6 | let y = &x;
| ^ borrowed value does not live long enough
7 | &**y
8 | };
| - `x` dropped here while still borrowed
9 | }
| - borrowed value needs to live until here
Что случилось? Если мы изменим let x = &mut i
на let x = &i
, он будет компилироваться нормально. С изменяемой ссылкой он терпит неудачу. Кажется, изменчивая ссылка - это нечто большее, чем просто ссылка!
Видите ли, здесь это заимствование &
для *x
(**y
), которое возвращается из внутренней области, а не только разыменованное значение *x
. Следовательно, наряду с тем, на что указывает x
, сам x
также должен жить (или быть скопированным) за пределы внутренней области. Думайте об этом как о заимствовании возвращаемого поля внутри struct
, которое находится за ссылкой. Что-то вроде этого, что даст ту же ошибку.
struct Foo{i:i32} fn main () { let x = { let f = Foo{i:1}; let f1 = &f; &f1.i }; }
Возвращаясь к прошлому, как мы знаем, изменяемая ссылка относится к типу, отличному от Copy
, что означает, что x
не может быть скопирован. Но и в этом случае его тоже нельзя вынести. Почему? потому что он находится за ссылкой y
и вы не можете выйти из заимствованного контекста. Но заимствовать &
в *x
(который**y)
выходит за пределы внутренней области видимости, что приводит к ситуации висячей ссылки, и, следовательно, компилятор справедливо кричит об ошибке (см. dropped here while still borrowed
в ошибке сообщение)
При неизменяемой ссылке (например, let x = &i
) нет вопроса о выходе из заимствованного контекста, так как &*x
(то есть &**y
) x
можно просто скопировать вместе с заимствованием, которое ведет к внешнему сфера.
Вот и все! Я надеюсь, что это затронуло несколько важных моментов в использовании изменяемых ссылок. Как видите, есть несколько сценариев, в которых изменяемые ссылки могут вас удивить. Но прелесть Rust в том, что он направляет вас своими сообщениями об ошибках, изо всех сил пытаясь заставить вас понять, что и почему так думает компилятор. Все для вашего же блага. Уберечь вас от мин! Тем не менее, есть еще много возможностей для улучшения сообщений об ошибках компилятора.