References and Borrowing

RustRustBeginner

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в References and Borrowing. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100393{{"References and Borrowing"}} rust/mutable_variables -.-> lab-100393{{"References and Borrowing"}} rust/string_type -.-> lab-100393{{"References and Borrowing"}} rust/function_syntax -.-> lab-100393{{"References and Borrowing"}} rust/expressions_statements -.-> lab-100393{{"References and Borrowing"}} rust/lifetime_specifiers -.-> lab-100393{{"References and Borrowing"}} rust/method_syntax -.-> lab-100393{{"References and Borrowing"}} end

References and Borrowing

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

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

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

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

Рисунок 4-5: Диаграмма &String s, указывающая на String s1

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

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

let s1 = String::from("hello");

let len = calculate_length(&s1);

Синтаксис &s1 позволяет нам создать ссылку, которая ссылается на значение s1, но не владеет им. Поскольку мы не владеем им, значение, на которое она ссылается, не будет уничтожено, когда ссылка перестанет использоваться.

Аналогично, сигнатура функции использует &, чтобы показать, что тип параметра s является ссылкой. Добавим некоторые поясняющие аннотации:

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

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

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

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

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: Попытка изменить заимствованное значение

Вот ошибка:

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

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

Mutable References

Мы можем исправить код из Listing 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");
}

Во-первых, мы меняем s на mut. Затем мы создаем изменяемую ссылку с помощью &mut s в месте, где мы вызываем функцию change, и обновляем сигнатуру функции, чтобы она принимала изменяемую ссылку с some_string: &mut String. Это делает очень ясно, что функция change изменит значение, которое она заимствует.

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

Filename: src/main.rs

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

let r1 = &mut s;
let r2 = &mut s;

println!("{r1}, {r2}");

Вот ошибка:

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

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

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

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

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

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

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

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

let r2 = &mut s;

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

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

let r1 = &s; // нет проблем
let r2 = &s; // нет проблем
let r3 = &mut s; // ОШИБКА!

println!("{r1}, {r2}, and {r3}");

Вот ошибка:

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; // ОШИБКА!
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

Уфф! Мы также не можем иметь изменяемую ссылку, если у нас есть неизменяемая ссылка на одно и то же значение.

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

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

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

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// переменные r1 и r2 не будут использоваться после этой точки

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

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

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

Dangling References

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

Попробуем создать промазывающую ссылку, чтобы увидеть, как Rust предотвращает их с помощью ошибки компиляции:

Filename: src/main.rs

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

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

    &s
}

Вот ошибка:

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 {
  |                ~~~~~~~~

Это сообщение об ошибке ссылается на функцию, которую мы еще не рассматривали: lifetimes. Мы обсудим lifetimes подробно в главе 10. Однако, если проигнорировать части о lifetimes, сообщение содержит ключ к тому, почему этот код является проблемой:

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 dangle() -> &String { // dangle возвращает ссылку на String

    let s = String::from("hello"); // s - это новый String

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

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

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

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

    s
}

Это работает без каких-либо проблем. Владение передается, и ничего не освобождается.

The Rules of References

Подведем итоги того, что мы обсуждали о ссылках:

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

Далее мы рассмотрим другой тип ссылки: срезы (slices).

Summary

Поздравляем! Вы завершили лабораторную работу по References and Borrowing. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.