Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

The issue with the tuple code in Listing 4-5 is that we have to return the String to the calling function so that we can still use the String after the call to calculate_length, because the String was moved into calculate_length. Instead, we can provide a reference to the String value. A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

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

Filename: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

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

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

First, notice that all the tuple code in the variable declaration and the function return value is gone. Second, note that we pass &s1 into calculate_length and, in its definition, we take &String rather than String. These ampersands represent references, and they allow you to refer to some value without taking ownership of it. Figure 4-6 depicts this concept.

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

Figure 4-6: A diagram of &String s pointing at 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()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

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

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

So, what happens if we try to modify something we’re borrowing? Try the code in Listing 4-6. Spoiler alert: It doesn’t work!

Filename: 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: Attempting to modify a borrowed value

Вот ошибка:

$ 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, чтобы позволить себе изменять заимствованное значение: внеся небольшие правки, мы можем получить изменяемую ссылку:

Filename: 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");
}

First, we change s to be mut. Then, we create a mutable reference with &mut s where we call the change function and update the function signature to accept a mutable reference with some_string: &mut String. This makes it very clear that the change function will mutate the value it borrows.

Mutable references have one big restriction: If you have a mutable reference to a value, you can have no other references to that value. This code that attempts to create two mutable references to s will fail:

Filename: 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` (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}, and {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!("{r1}, {r2}, and {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}");
}

The scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created. These scopes don’t overlap, so this code is allowed: The compiler can tell that the reference is no longer being used at a point before the end of the scope.

Even though borrowing errors may be frustrating at times, remember that it’s the Rust compiler pointing out a potential bug early (at compile time rather than at runtime) and showing you exactly where the problem is. Then, you don’t have to track down why your data isn’t what you thought it was.

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

In languages with pointers, it’s easy to erroneously create a dangling pointer—a pointer that references a location in memory that may have been given to someone else—by freeing some memory while preserving a pointer to that memory. In Rust, by contrast, the compiler guarantees that references will never be dangling references: If you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.

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

Filename: 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, 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 {
  |

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

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

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

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

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

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

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

    &s // мы возвращаем ссылку s на значение типа String
} // Here, s goes out of scope and is dropped, so 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
}

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

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

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

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

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