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

Основная проблематика в подходе с использованием кортежа в листинге 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, nothing happens.

Область действия, в которой 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

Эта ошибка сообщает об ещё не освещённой нами возможности языка Rust: времени жизни переменной (lifetime). Мы расскажем подробнее об этой возможности в Главе 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
}

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

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

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

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

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