Посилання та запозичення (References and Borrowing)
Проблема з кодом кортежу в Лістингу (Listing) 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. Ці амперсанди (ampersands) позначають посилання, і вони дозволяють вам
посилатися на певне значення, не беручи його у володіння. Рисунок (Figure) 4-6
зображує цю концепцію.
Рисунок (Figure) 4-6: Діаграма &String s, що вказує на
String s1 (A diagram of &String s pointing at 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 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 не має
володіння. Коли функції мають посилання як параметри замість фактичних
значень, нам не потрібно буде повертати значення, щоб повернути володіння,
тому що ми ніколи не мали володіння.
Дію створення посилання ми називаємо запозиченням. Як і в реальному житті, якщо людина чимось володіє, ви можете позичити це у неї. Коли ви закінчите, ви маєте повернути це. Ви цим не володієте.
Отже, що станеться, якщо ми спробуємо змінити щось, що ми запозичуємо? Спробуйте код у Лістингу (Listing) 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
Так само, як змінні за замовчуванням є незмінними, такими є й посилання. Нам не дозволено змінювати те, на що ми маємо посилання.
Змінні посилання (Mutable References)
Ми можемо виправити код з Лістингу (Listing) 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. Потім ми створюємо змінне посилання за
допомогою &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, растацеанці (Rustaceans), мають труднощі, тому що більшість мов дозволяють вам змінювати коли завгодно. Перевага наявності цього обмеження полягає в тому, що Rust може запобігати станам гонки даних (data race) під час компіляції. Стан гонки даних — це подібно до умови гонки і трапляється, коли відбуваються такі три поведінки:
- Два або більше вказівники отримують доступ до одних і тих самих даних одночасно.
- Принаймні один із вказівників використовується для запису в дані.
- Немає механізму, який використовується для синхронізації доступу до даних.
Стани гонки даних спричиняють невизначену поведінку і можуть бути складними для діагностики та виправлення, коли ви намагаєтеся знайти їх під час виконання; 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 вказує на потенційну помилку на ранньому етапі (під час компіляції, а не під час виконання) і показує вам точно, де проблема. Тоді вам не потрібно буде шукати, чому ваші дані не такі, як ви думали.
Висячі посилання (Dangling References)
У мовах із вказівниками легко помилково створити висячий вказівник (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
Це повідомлення про помилку стосується можливості, яку ми ще не розглядали: часів життя. Ми детально обговоримо часи життя в Розділі 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
}
Це працює без жодних проблем. Власність переміщується назовні, і нічого не деалокується.
Правила посилань (The Rules of References)
Підсумуймо те, що ми обговорили щодо посилань:
- У будь-який момент часу ви можете мати або одне змінне посилання, або будь-яку кількість незмінних посилань.
- Посилання завжди мають бути дійсними.
Далі ми розглянемо інший вид посилання: зрізи.