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

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

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

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

Файл: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

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

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

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

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

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

Примечание: противоположность взятию ссылки через & — разыменование (dereferencing), для которого служит оператор разыменования *. Несколько примеров его использования мы увидим в главе 8, а подробно разыменование разберём в главе 15.

Посмотрим внимательнее на вызов функции:

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {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 '{s1}' is {len}.");
}

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

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

Создание ссылки называется заимствованием (borrowing). Как и в жизни: если у кого-то есть вещь, вы можете взять её взаймы. Попользовались — нужно вернуть. Вещь не ваша.

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

Файл: src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
Листинг 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
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so it 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 можно исправить так, чтобы заимствованное значение можно было менять. Для этого достаточно пары небольших правок — и использовать изменяемую ссылку (mutable reference):

Файл: 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). Затем там, где вызывается функция change, создаём изменяемую ссылку &mut s, а в сигнатуре функции указываем, что она принимает изменяемую ссылку: 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` (bin "ownership") due to 1 previous error

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

Запрет на несколько одновременных изменяемых ссылок на одни и те же данные позволяет изменять данные, но под строгим контролем. Новичкам в Rust это поначалу даётся тяжело: в большинстве языков менять данные можно когда угодно. Зато благодаря этому ограничению Rust предотвращает гонки данных (data races) ещё на этапе компиляции. Гонка данных похожа на состояние гонки и возникает, когда одновременно выполняются три условия:

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

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

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

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

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

    let r2 = &mut s;
}

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

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; // без проблем
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // без проблем
6 |     let r3 = &mut s; // БОЛЬШАЯ ПРОБЛЕМА
  |              ^^^^^^ 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} and {r2}");
    // Дальше r1 и r2 больше не используются.

    let r3 = &mut s; // без проблем
    println!("{r3}");
}

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

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

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

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

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

Это сообщение упоминает возможность, которую мы ещё не разбирали, — времена жизни (lifetimes). Подробно о них мы поговорим в главе 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 возвращает ссылку на String

    let s = String::from("hello"); // s — новая String

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

Строка s создаётся внутри dangle, поэтому, когда код dangle завершится, память s будет освобождена. А мы пытаемся вернуть ссылку на неё. Значит, эта ссылка указывала бы на уже недействительную String. Так не пойдёт! Rust этого не допустит.

Решение — вернуть саму String:

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

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

    s
}

Так всё работает без проблем: владение перемещается наружу, и ничего не освобождается.

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

Подытожим, что мы узнали о ссылках:

  • В каждый момент времени может быть либо одна изменяемая ссылка, либо сколько угодно неизменяемых.
  • Ссылки всегда должны быть действительными.

Дальше мы рассмотрим другой вид ссылок — срезы.