Прежде чем перейти к пониманию изменяемой ссылки, давайте взглянем на очень простой пример на 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 в том, что он направляет вас своими сообщениями об ошибках, изо всех сил пытаясь заставить вас понять, что и почему так думает компилятор. Все для вашего же блага. Уберечь вас от мин! Тем не менее, есть еще много возможностей для улучшения сообщений об ошибках компилятора.