Посилання і позичання

Проблема з кодом, що використовує кортежі, з Блоку коду 4-5 полягає в тому, що ми маємо повертати String у функцію, що викликає, щоб можна було використовувати String після виклику calculate_length, бо String переміщується до calculate_length. Натомість ми можемо надати посилання на значення String. Посилання - це як вказівник, тобто адреса, за якою можна перейти, щоб отримати дані, збережені за цією адресою; ці дані є володінням якоїсь іншої змінної. На відміну від вказівника, посилання гарантовано вказує на коректне значення певного типу весь час існування цього посилання.

Ось як ви маєте визначити і використовувати функцію 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 описує цю концепцію.

Три таблиці: таблиця s містить лише вказівник на таблицю
для s1. Таблиця для s1 містить дані стеку для 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, it is not dropped.

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

Операція створення посилання зветься позичанням. Як і в справжньому житті, якщо особа володіє чимось, ви можете це позичити у неї , а коли річ вам стане не потрібна, треба її віддати. Бо вона вам не належить.

Що ж станеться, якщо ми спробуємо змінити щось, що ми позичили? Спробуйте запустити код з Блоку коду 4-6. Обережно, спойлер: він не працює!

Файл: src/main.rs

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Блок коду 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

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

Посилання, так само як і змінні, за умовчанням є немутабельними. Ми не можемо змінити щось, на що ми маємо посилання.

Мутабельні посилання

We can fix the code from Listing 4-6 to allow us to modify a borrowed value with just a few small tweaks that use, instead, a mutable reference:

Файл: 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, не спрацює:

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

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

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

Це обмеження, що забороняє кілька мутабельних посилань на одні й ті самі дані в один час, дозволяє їх змінювати, але під пильним контролем. Це те, із чим борються початківці-растацеанці, бо більшість мов дозволяють вам змінювати дані коли завгодно. Перевага цього обмеження в тому, що Rust може запобігти гонитві даних під час компіляції. Гонитва даних подібна до стану гонитви й стається, коли мають місце такі три умови:

  • Два чи більше вказівників мають доступ до одних даних у один і той самий час.
  • Щонайменше один зі вказівників використовується для запису даних.
  • Не застосовується жодних механізмів синхронізації доступу до даних.

Гонитви даних викликають невизначену поведінку і їх може бути складно діагностувати та виправляти, коли ви намагаєтесь відстежити їх під час роботи програми; Rust запобігає проблемі, відмовляючись компілювати код з гонитвою даних!

As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:

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!("{}, {}, 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

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

Хух! Не виходить також мати мутабельне посилання, коли в нас є немутабельне посилання на це ж значення.

Користувачі немутабельного посилання не очікують, що його значення несподівано зміниться прямо під час використання. Втім, багато немутабельних посилань допустимі, бо жоден з тих, хто просто читає дані, не може вплинути на те, що інші теж читають ці ж дані.

Зверніть увагу, що область видимості посилання починається з місця його проголошення і продовжується до останнього разу, коли посилання використовується. Наприклад, цей код компілюється тому, що останнє використання немутабельних посилань у println! відбувається до проголошення мутабельного посилання:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, 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 натомість компілятор гарантує, що посилання ніколи не стануть висячими: якщо ви маєте посилання на певні дані, компілятор пересвідчиться, що дані не вийдуть з області видимості раніше за посилання на ці дані.

Let’s try to create a dangling reference to see how Rust prevents them with a compile-time error:

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

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

Це повідомлення про помилку посилається на особливість, про яку ми ще не розповідали: час існування. Ми обговоримо часи існування детальніше у Розділі 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
}

Це працює без проблем. Володіння переміщується з функції, і нічого не звільняється.

Правила посилань

Ще раз повторимо, що ми обговорили про посилання:

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

Далі ми поглянемо на інший тип посилань: слайси.