Ссылки и заимствование
Проблема с кортежем в листинге 4-5 в том, что String приходится возвращать
обратно вызывающей функции. Иначе после вызова calculate_length мы не смогли
бы пользоваться этой строкой — ведь она переместилась в 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()
}
Во-первых, заметьте: весь код с кортежами — и в объявлении переменной, и в
возвращаемом значении функции — исчез. Во-вторых, в calculate_length мы
передаём &s1, а в её определении принимаем &String, а не String. Эти
амперсанды и обозначают ссылки: они позволяют обратиться к значению, не
забирая владения им. Это показано на рисунке 4-6.
Рисунок 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. Спойлер: не сработает!
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 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):
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, не
скомпилируется:
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 не даёт этого сделать, выдавая ошибку компиляции:
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:
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
}
Так всё работает без проблем: владение перемещается наружу, и ничего не освобождается.
Правила ссылок
Подытожим, что мы узнали о ссылках:
- В каждый момент времени может быть либо одна изменяемая ссылка, либо сколько угодно неизменяемых.
- Ссылки всегда должны быть действительными.
Дальше мы рассмотрим другой вид ссылок — срезы.