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

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

А вот пример того, как можно было бы определить и использовать функцию 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 действительна, является такой же как у любого другого параметра функции, но мы не освобождаем значение на которое указывает ссылка, когда эта переменная уходит из области видимости, так как нет владения этим значением. Когда функции используют в параметрах ссылки вместо значений, то не нужно возвращать значения, чтобы вернуть владение обратно, по причине полного отсутствия такого владения.

Наличие ссылок в качестве параметров функций называется заимствованием (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");
}

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

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

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

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

Можно исправить ошибку в коде листинга 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 и принять изменяемую ссылку с помощью some_string: &mut String.

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

Файл: 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

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

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

Польза от этого ограничения в том, что Rust может предотвратить возникновение эффекта гонок данных во время компиляции. Эффект гонок данных (data race) является похожим на состояние гонки и возникает, когда происходят три следующих поведения:

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

Ситуация гонок данных приводит к неопределённому поведению (undefined behavior - UB), которая является трудно диагностируемой и трудно исправляемой проблемой при попытке отследить её во время выполнения; 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;
}

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

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

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

Вот так! Мы также не можем иметь изменяемую ссылочную переменную, пока существует неизменяемая ссылочная переменная. Пользователи неизменяемой ссылки не ожидают внезапного изменения значения на которые она указывает! Тем не менее, наличие множества неизменяемых переменных допускается, т.к. ни один из читающих данные не может изменить данные, которые все остальные также читают.

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

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point

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

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

Не смотря на то, что ошибки заимствования могут иногда вызывать разочарование, помните, что компилятор 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 {
  |                ^^^^^^^^

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

Эта ошибка сообщает об ещё не освещённой нами возможности языка 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
}

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

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

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

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

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