Ссылки и заимствование
Проблема кода с кортежем в листинге 4-5 состоит в том, что нам приходится
возвращать String вызывающей функции, чтобы мы всё ещё могли использовать
String после вызова calculate_length, потому что значение String было
перемещено в calculate_length. Вместо этого мы можем предоставить ссылку
на значение String. Ссылка похожа на указатель тем, что это адрес, по
которому можно перейти для доступа к данным, хранящимся по этому адресу; этими
данными владеет какая-то другая переменная. В отличие от указателя, ссылка
гарантированно указывает на действительное значение определённого типа
на протяжении всей жизни этой ссылки.
Вот как можно определить и использовать функцию calculate_length, которая
принимает ссылку на объект в качестве параметра вместо того, чтобы забирать
владение значением:
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-6 иллюстрирует эту концепцию.
Рисунок 4-6: Схема, на которой &String s указывает
на String s1
Примечание: операция, противоположная созданию ссылки с помощью
&, — разыменование, которое выполняется оператором разыменования*. Мы увидим несколько способов использования оператора разыменования в Главе 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 is a reference to a 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 не владеет им.
Когда функции принимают ссылки как параметры вместо самих значений, нам не
нужно возвращать значения, чтобы вернуть владение, потому что владения у нас
никогда не было.
Действие создания ссылки мы называем заимствованием. Как и в реальной жизни, если человек владеет чем-то, вы можете это у него одолжить. Когда вы закончите, нужно вернуть это обратно. Вы этим не владеете.
Что же произойдёт, если мы попытаемся изменить то, что заимствуем? Попробуйте код из листинга 4-6. Спойлер: это не работает!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Вот ошибка:
$ 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, чтобы разрешить изменять заимствованное значение, внеся всего несколько небольших правок и используя изменяемую ссылку:
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, завершится ошибкой:
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!, но между созданием этой изменяемой ссылки и её использованием мы
попытались создать другую изменяемую ссылку в r2, которая заимствует те же
данные, что и r1.
Ограничение, запрещающее несколько изменяемых ссылок на одни и те же данные одновременно, допускает изменение, но в очень контролируемом виде. Новым разработчикам Rust с этим бывает сложно, потому что большинство языков позволяют изменять данные когда угодно. Польза этого ограничения в том, что 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!("{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; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
Области действия неизменяемых ссылок r1 и r2 заканчиваются после
println!, где они используются в последний раз, то есть до создания
изменяемой ссылки r3. Эти области не пересекаются, поэтому такой код
разрешён: компилятор может определить, что ссылка больше не используется
в точке до конца области видимости.
Хотя ошибки заимствования иногда могут раздражать, помните: это компилятор Rust заранее указывает на потенциальную ошибку (во время компиляции, а не во время выполнения) и точно показывает, где находится проблема. Тогда вам не придётся выяснять, почему ваши данные оказались не такими, какими вы их ожидали.
Висячие ссылки
В языках с указателями легко ошибочно создать висячий указатель — указатель, который ссылается на место в памяти, уже, возможно, переданное кому-то ещё, — освободив часть памяти, но сохранив указатель на эту память. В Rust, напротив, компилятор гарантирует, что ссылки никогда не будут висячими ссылками: если у вас есть ссылка на какие-то данные, компилятор убедится, что данные не выйдут из области видимости раньше, чем ссылка на эти данные.
Попробуем создать висячую ссылку, чтобы увидеть, как Rust предотвращает это ошибкой времени компиляции:
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
Давайте подробнее рассмотрим, что именно происходит на каждом этапе нашего
кода dangle:
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, 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
}
Это работает без проблем. Владение перемещается из функции наружу, и ничего не освобождается.
Правила ссылок
Подведём итог тому, что мы обсудили о ссылках:
- В любой момент времени у вас может быть либо одна изменяемая ссылка, либо любое количество неизменяемых ссылок.
- Ссылки всегда должны быть действительными.
Далее мы рассмотрим другой вид ссылок: срезы.