Ссылочные переменные и заимствование

Проблема с кодом кортежа в Листинге 4-5 заключается в том, что мы должны вернуть String из вызванной функции, чтобы мы могли использовать String после вызова calculate_length, потому что String была перемещена в calculate_length. Вместо этого мы можем предоставить ссылку на значение String. Ссылка похожа на указатель в том смысле, что это адрес, по которому мы можем следовать, чтобы получить доступ к данным, хранящимся по этому адресу; эти данные принадлежат какой-то другой переменной. В отличие от указателя, ссылка гарантированно указывает на допустимое значение определённого типа в течение всего срока существования этой ссылки.

Вот как вы могли бы определить и использовать функцию calculate_length, имеющую ссылку на объект в качестве параметра вместо того, чтобы брать на себя ответственность за значение:

Файл: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Во-первых, обратите внимание, что весь код кортежа в объявлении переменной и возвращаемое значение функции исчезли. Во-вторых, обратите внимание, что мы передаём &s1 в calculate_length и в его определении мы берём &String а не String. Эти амперсанды представляют собой ссылки, и они позволяют вам ссылаться на некоторое значение, не принимая владение им. Рисунок 4-5 изображает эту концепцию.

&String s pointing at String s1

Картинка 4-5: Диаграмма для &String s указывающей на String s1

Примечание. Противоположностью ссылки с использованием & является разыменование, выполняемое с помощью оператора разыменования *. Мы увидим некоторые варианты использования оператора разыменования в Главе 8 и обсудим детали разыменования в Главе 15.

Давайте подробнее рассмотрим механизм вызова функции:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

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

Точно так же сигнатура функции использует & для указания того, что тип параметра s является ссылкой. Добавим несколько поясняющих аннотаций:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

Область действия, в которой s такая же, как и область действия любого параметра функции, но значение, на которое указывает ссылка, не удаляется, когда s перестаёт использоваться, потому что s не является владельцем. Когда функции имеют ссылки в качестве параметров вместо фактических значений, нам не нужно возвращать значения, чтобы вернуть право владения, потому что мы никогда не владели ими.

Мы называем действие создания ссылки заимствованием. Как и в реальной жизни, если человек чем-то владеет, вы можете это у него позаимствовать. Когда вы закончите, вы должны вернуть его. Вы им не владеете.

Так что же произойдёт, если мы попытаемся изменить что-то, что мы заимствуем? Попробуйте запустить код из Листинга 4-6. Спойлер: это не сработает!

Файл: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Listing 4-6: Попытка модификации заимствованной переменной

Вот ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

Как переменные неизменяемы по умолчанию, так и ссылки. Нам не разрешено изменять что-то, на что у нас есть ссылка.

Изменяемые ссылочные переменные

Мы можем исправить код из листинга 4-6, чтобы позволить нам изменять заимствованное значение с помощью всего лишь нескольких небольших настроек, которые вместо этого используют изменяемую ссылку:

Файл: src/main.rs

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Сначала мы меняем s на mut. Затем мы создаём изменяемую ссылку с помощью &mut s у которой вызываем change и обновляем сигнатуру функции, чтобы принять изменяемую ссылку с помощью some_string: &mut String. Это даёт понять, что change изменит значение, которое она заимствует.

Изменяемые ссылки имеют одно большое ограничение: если у вас есть изменяемая ссылка на значение, у вас не может быть других ссылок на это значение. Этот код, который пытается создать две изменяемые ссылки на s, завершится ошибкой:

Файл: src/main.rs

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

Описание ошибки:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

Эта ошибка говорит о том, что этот код недействителен, потому что мы не можем заимствовать s как изменяемые более одного раза в один момент. Первое изменяемое заимствование находится в r1 и должно длиться до тех пор, пока оно не будет использовано в println!, но между созданием этой изменяемой ссылки и её использованием мы попытались создать другую изменяемую ссылку в r2, которая заимствует те же данные, что и r1.

Ограничение, предотвращающее одновременное использование нескольких изменяемых ссылок на одни и те же данные, допускает изменение, но очень контролируемым образом. Это то, с чем борются новые Rustaceans, потому что большинство языков позволяют изменять значение, когда захотите. Преимущество этого ограничения заключается в том, что Rust может предотвратить гонки данных во время компиляции. Гонка данных похожа на состояние гонки и происходит, когда возникают следующие три сценария:

  • два или больше указателей используют те же данные в одно и тоже время,
  • минимум один указатель используется для записи данных,
  • отсутствуют механизмы для синхронизации доступа к данным.

Гонки данных вызывают неопределённое поведение и их может быть сложно диагностировать и исправить, когда вы пытаетесь отследить их во время выполнения; Rust предотвращает эту проблему, отказываясь компилировать код с гонками данных!

Как всегда, мы можем использовать фигурные скобки для создания новой области видимости, позволяющей использовать несколько изменяемых ссылок, но не одновременно :

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Rust применяет аналогичное правило для комбинирования изменяемых и неизменяемых ссылок. Этот код приводит к ошибке:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

Ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Вау! У нас также не может быть изменяемой ссылки, пока у нас есть неизменяемая ссылка на то же значение.

Пользователи неизменяемой ссылки не ожидают, что значение внезапно изменится из-под них! Однако разрешены множественные неизменяемые ссылки, потому что никто, кто просто читает данные, не может повлиять на чтение данных кем-либо ещё.

Обратите внимание, что область действия ссылки начинается с того места, где она была введена, и продолжается до последнего использования этой ссылки. Например, этот код будет компилироваться, потому что последнее использование неизменяемых ссылок println!, происходит до того, как вводится изменяемая ссылка:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);
}

Области неизменяемых ссылок r1 и r2 заканчиваются после println! где они использовались в последний раз, то есть до создания изменяемой ссылки r3. Эти области не пересекаются, поэтому этот код разрешён. Способность компилятора сообщить, что ссылка больше не используется в точке до конца области видимости, называется нелексическим временем жизни (сокращённо NLL), и вы можете прочитать об этом больше в The Edition Guide.

Несмотря на то, что ошибки заимствования могут иногда вызывать разочарование, помните, что это компилятор Rust заранее указывает на потенциальную ошибку (во время компиляции, а не во время выполнения) и точно показывает, в чем проблема. Тогда вам не придётся выяснять, почему ваши данные не такие, как вы думали.

Недействительные ссылки

В языках с указателями весьма легко ошибочно создать недействительную, висячую (dangling) ссылку. Ссылку указывающую на участок памяти, который мог быть передан кому-то другому, путём освобождения некоторой памяти при сохранении указателя на эту память. Rust компилятор гарантирует, что ссылки никогда не станут недействительными: если у вас есть ссылка на какие-то данные, компилятор обеспечит что эти данные не выйдут из области видимости прежде, чем из области видимости исчезнет ссылка.

Попытаемся смоделировать подобную, висячую ссылку, появление которой компилятор предотвратит:

Файл: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Здесь ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

Это сообщение об ошибке относится к особенности языка, которую мы ещё не рассмотрели: времени жизни. Мы подробно обсудим времена жизни в Главе 10. Но если вы не будете обращать внимания на части, касающиеся времени жизни, сообщение действительно содержит ключ к тому, почему этот код является проблемой:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Давайте подробнее рассмотрим, что именно происходит на каждом этапе нашего кода dangle:

Файл: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

Поскольку s создаётся внутри dangle, когда код dangle будет завершён, s будет освобождён. Но мы попытались вернуть ссылку на него. Это означает, что эта ссылка будет указывать на недопустимую String. Это не хорошо! Rust не позволит нам сделать это.

Решением является вернуть непосредственно String:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Это работает без проблем. Владение перемещено, и ничего не освобождено.

Правила работы с ссылками

Давайте повторим все, что мы обсудили про ссылки:

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

В следующей главе мы рассмотрим другой тип ссылочных переменных - срезы.