Ссылки и заимствование

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

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

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

    let len = calculate_length(&s1);

    println!("Длина '{s1}' равна {len}.");
}

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

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

Три таблицы: таблица s содержит только указатель на таблицу
s1. Таблица s1 содержит данные на стеке строки s1 и указывает на
текстовые данные в куче.

Рисунок 4-6: Схема строения &String s, указывающей на String s1

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

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

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

    let len = calculate_length(&s1);

    println!("Длина '{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!("Длина '{s1}' равна {len}.");
}

fn calculate_length(s: &String) -> usize { // s — ссылка на String
    s.len()
} // Здесь s входит в область видимости. Но поскольку s не владеет тем, на что
  // ссылается, значение не удаляется.

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

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

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

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

    change(&s);
}

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

Вот ошибка:

$ 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
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

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

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

Изменяемые ссылки

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

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, не скомпилируется:

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` (bin "ownership") due to 1 previous error

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

The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion. It’s something that new Rustaceans struggle with because most languages let you mutate whenever you’d like. The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:

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

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

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

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

    {
        let r1 = &mut s;
    } // здесь r1 покидает область видимости, так что мы можем без проблем создать новую ссылку.

    let r2 = &mut s;
}

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

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

    let r1 = &s; // всё хорошо
    let r2 = &s; // всё хорошо
    let r3 = &mut s; // ВСЁ ПЛОХО

    println!("{}, {} и {}", 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; // всё хорошо
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // всё хорошо
6 |     let r3 = &mut s; // ВСЁ ПЛОХО
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {} и {}", r1, r2, r3);
  |                                -- immutable borrow later used here

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

Да. Мы, в том числе, не можем иметь изменяемую ссылку, пока у нас действительна неизменяемая ссылка на то же значение.

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

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

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

    let r1 = &s; // всё хорошо
    let r2 = &s; // всё хорошо
    println!("{r1} и {r2}");
    // Переменные r1 и r2 далее не используются.

    let r3 = &mut s; // всё хорошо
    println!("{r3}");
}

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

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

Висячие ссылки

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

Давайте попробуем создать висячую ссылку, чтобы увидеть, как Rust предотвращает их появление — сразу на этапе компиляции:

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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

error[E0515]: cannot return reference to local variable `s`
 --> src/main.rs:8:5
  |
8 |     &s
  |     ^^ returns a reference to data owned by the current function

Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors

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

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

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

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

fn dangle() -> &String { // dangle возвращает ссылку на String

    let s = String::from("hello"); // в s записывается новое значение типа String

    &s // мы возвращаем ссылку s на значение типа String
} // Здесь s покидает область видимости, и её память высвобождается
  // Беда!

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

Решением будет вернуть непосредственно само значение String:

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

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

    s
}

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

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

Давайте повторим все, что мы узнали о ссылках:

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

В следующем разделе мы рассмотрим другой тип ссылок — срезы.