Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Що таке володіння / власність (Ownership)

Володіння (Ownership) — це набір правил, які керують тим, як програма Rust керує пам’яттю. Усі програми мають керувати способом, у який вони використовують пам’ять комп’ютера під час виконання. Деякі мови мають збирання сміття, яке регулярно шукає пам’ять, що більше не використовується, поки програма працює; в інших мовах програміст має явно виділяти і звільняти пам’ять. Rust використовує третій підхід: пам’ять керується через систему володіння (ownership system) з набором правил, які перевіряє компілятор. Якщо будь-яке з правил порушено, програма не скомпілюється. Жодна з можливостей володіння (ownership) не сповільнить вашу програму під час виконання.

Оскільки володіння (ownership) — це нова концепція для багатьох програмістів, до неї справді потрібно трохи часу, щоб звикнути. Добра новина полягає в тому, що чим досвідченішими ви стаєте з Rust і правилами системи володіння (ownership system), тим легше вам буде природно розробляти код, який є безпечним і ефективним. Продовжуйте!

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

Стек і купа (The Stack and the Heap)

Багато мов програмування не вимагають, щоб ви дуже часто думали про стек і купу. Але в системній мові програмування, як-от Rust, те, чи значення перебуває у стеку чи в купі, впливає на те, як поводиться мова і чому вам доводиться робити певні рішення. Частини володіння (ownership) буде описано у зв’язку зі стеком і купою пізніше в цьому розділі, тож ось коротке пояснення для підготовки.

І стек, і купа є частинами пам’яті, доступними для використання вашим кодом під час виконання, але вони структуровані по-різному. Стек зберігає значення в тому порядку, в якому він їх отримує, і видаляє значення у зворотному порядку. Це називається останній прийшов — перший пішов (LIFO). Подумайте про стопку тарілок: коли ви додаєте ще тарілки, ви кладете їх на верхівку стопки, а коли вам потрібна тарілка, ви берете одну згори. Додавати або видаляти тарілки із середини чи знизу працювало б не так добре! Додавання даних називається проштовхуванням у стек, а видалення даних називається виштовхуванням зі стеку. Усі дані, що зберігаються у стеку, мають мати відомий, фіксований розмір. Дані з невідомим розміром під час компіляції або розміром, який може змінюватися, мають зберігатися в купі замість цього.

Купа менш упорядкована: коли ви розміщуєте дані в купі, ви запитуєте певний обсяг простору. Алокатор пам’яті знаходить порожнє місце в купі, достатньо велике, позначає його як таке, що використовується, і повертає вказівник, який є адресою цього місця. Цей процес називається виділенням у купі і інколи скорочується просто до виділення (проштовхування значень у стек не вважається виділенням). Оскільки вказівник на купу має відомий, фіксований розмір, ви можете зберегти вказівник у стеку, але коли ви хочете отримати самі дані, ви маєте слідувати за вказівником. Подумайте про те, як вас садять у ресторані. Коли ви входите, ви повідомляєте кількість людей у вашій групі, а хост знаходить порожній стіл, який вмістить усіх, і проводить вас туди. Якщо хтось із вашої групи запізниться, він може запитати, де вас посадили, щоб знайти вас.

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

Доступ до даних у купі загалом повільніший, ніж доступ до даних у стеку, тому що вам потрібно слідувати за вказівником, щоб дістатися туди. Сучасні процесори працюють швидше, якщо вони менше стрибають пам’яттю. Продовжуючи аналогію, уявіть собі офіціанта в ресторані, який приймає замовлення від багатьох столів. Найефективніше отримати всі замовлення за одним столом, перш ніж переходити до наступного столу. Брати замовлення за столом A, потім замовлення за столом B, потім одне з A знову, а потім одне з B знову було б значно повільнішим процесом. Так само процесор зазвичай може краще виконувати свою роботу, якщо він працює з даними, які близько розташовані до інших даних (як це є у стеку), а не далі від них (як це може бути в купі).

Коли ваш код викликає функцію, значення, передані у функцію (включно, потенційно, з вказівниками на дані в купі), і локальні змінні функції проштовхуються у стек. Коли функція завершується, ці значення виштовхуються зі стеку.

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

Правила володіння (Ownership Rules)

Спочатку давайте поглянемо на правила володіння (ownership rules). Тримайте ці правила в голові, коли ми розглядатимемо приклади, що їх ілюструють:

  • Кожне значення в Rust має власника.
  • Одночасно може бути лише один власник.
  • Коли власник виходить за межі області видимості, значення буде знищено.

Область видимості змінної (Variable Scope)

Тепер, коли ми вже пройшли базовий синтаксис Rust, ми не включатимемо весь код fn main() { у приклади, тож якщо ви йдете за текстом, не забудьте вручну помістити наступні приклади всередину функції main. У результаті наші приклади будуть трохи лаконічнішими, дозволяючи нам зосередитися на самих деталях, а не на допоміжному коді.

Як перший приклад володіння (ownership) ми розглянемо область видимості деяких змінних. Область видимості — це діапазон у програмі, протягом якого елемент є дійсним. Візьміть таку змінну:

#![allow(unused)]
fn main() {
let s = "hello";
}

Змінна s посилається на рядковий літерал, де значення рядка жорстко закодоване (hardcoded) в тексті нашої програми. Змінна є дійсною від моменту, коли її оголошено, і до кінця поточної області видимості. У Лістингу (Listing) 4-1 показано програму з коментарями, які позначають, де змінна s була б дійсною.

fn main() {
    {                      // s is not valid here, since it's not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

Іншими словами, тут є два важливі моменти в часі:

  • Коли s входить у область видимості, вона є дійсною.
  • Вона залишається дійсною, доки не вийде з області видимості.

На цьому етапі взаємозв’язок між областями видимості та моментом, коли змінні є дійсними, подібний до такого в інших мовах програмування. Тепер ми розвинемо це розуміння, увівши тип String.

Тип String (The String Type)

Щоб ілюструвати правила володіння (ownership), нам потрібен тип даних, який є складнішим, ніж ті, що ми розглядали в розділі “Типи даних” (Data Types) з Розділу 3. Попередньо розглянуті типи мають відомий розмір, можуть зберігатися у стеку й виштовхуватися зі стеку, коли їхня область видимості завершується, і можуть швидко та тривіально копіюватися, щоб створити новий, незалежний екземпляр, якщо іншій частині коду потрібно використати те саме значення в іншій області видимості. Але ми хочемо подивитися на дані, що зберігаються в купі, і дослідити, як Rust знає, коли очищати ці дані, а тип String — чудовий приклад.

Ми зосередимося на тих частинах String, які пов’язані з володінням (ownership). Ці аспекти також застосовуються до інших складних типів даних, незалежно від того, чи надаються вони стандартною бібліотекою, чи створені вами. Ми обговоримо аспекти String, не пов’язані з володінням (ownership), у Розділі 8.

Ми вже бачили рядкові літерали, де значення рядка жорстко закодоване у нашій програмі. Рядкові літерали зручні, але вони не підходять для кожної ситуації, в якій ми можемо захотіти використовувати текст. Одна з причин полягає в тому, що вони незмінні. Інша полягає в тому, що не кожне значення рядка можна знати, коли ми пишемо наш код: наприклад, що якби ми хотіли приймати введення користувача і зберігати його? Саме для таких ситуацій у Rust є тип String. Цей тип керує даними, виділеними в купі, і тому може зберігати обсяг тексту, який нам невідомий під час компіляції. Ви можете створити String з рядкового літерала за допомогою функції from, ось так:

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

Оператор подвійної двокрапки :: дозволяє нам помістити цю конкретну функцію from у простір імен типу String замість використання якогось імені на кшталт string_from. Ми обговоримо цей синтаксис детальніше в розділі “Methods” Розділу 5, а також коли говоритимемо про простори імен за допомогою модулів у “Paths for Referring to an Item in the Module Tree” у Розділі 7.

Цей тип рядка можна змінювати:

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

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{s}"); // this will print `hello, world!`
}

Отже, у чому тут різниця? Чому String можна змінювати, а літерали — ні? Різниця полягає в тому, як ці два типи поводяться з пам’яттю.

Пам’ять і виділення (Memory and Allocation)

У випадку рядкового літерала ми знаємо вміст під час компіляції, тож текст жорстко закодований безпосередньо у фінальний виконуваний файл. Саме тому рядкові літерали швидкі та ефективні. Але ці властивості походять лише від незмінності рядкового літерала. На жаль, ми не можемо помістити блок пам’яті в бінарний файл для кожного фрагмента тексту, розмір якого невідомий під час компіляції і який може змінюватися під час виконання програми.

У випадку типу String, щоб підтримати змінюваний, зростаючий фрагмент тексту, нам потрібно виділити в купі обсяг пам’яті, невідомий під час компіляції, щоб утримувати вміст. Це означає:

  • Потрібно запитати пам’ять у алокатора пам’яті під час виконання.
  • Нам потрібен спосіб повернути цю пам’ять алокатору, коли ми закінчимо з нашим String.

Перша частина робиться нами: коли ми викликаємо String::from, його реалізація запитує потрібну йому пам’ять. Це майже універсально в мовах програмування.

Однак друга частина відрізняється. У мовах зі збирачем сміття (GC) збирач сміття відстежує і очищає пам’ять, яка більше не використовується, і нам не потрібно про це думати. У більшості мов без GC це наша відповідальність — визначити, коли пам’ять більше не використовується, і викликати код, щоб явно звільнити її, так само як ми робили для запиту на її виділення. Історично зробити це правильно було складною програмістською проблемою. Якщо ми забудемо, ми марнуватимемо пам’ять. Якщо зробимо це занадто рано, у нас буде недійсна змінна. Якщо зробимо це двічі, це теж помилка. Нам потрібно поєднати рівно один allocate з рівно одним free.

Rust обирає інший шлях: пам’ять автоматично повертається, коли змінна, що володіє нею, виходить за межі області видимості. Ось версія нашого прикладу з областю видимості з Лістингу (Listing) 4-1, але з String замість рядкового літерала:

fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

Існує природний момент, коли ми можемо повернути пам’ять, потрібну нашому String, алокатору: коли s виходить за межі області видимості. Коли змінна виходить за межі області видимості, Rust викликає для нас спеціальну функцію. Ця функція називається drop, і саме в ній автор String може розмістити код для повернення пам’яті. Rust викликає drop автоматично при закривальній фігурній дужці.

Примітка: у C++ цей шаблон звільнення ресурсів наприкінці часу життя елемента інколи називається Resource Acquisition Is Initialization (RAII). Функція drop у Rust буде вам знайома, якщо ви використовували шаблони RAII.

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

Змінні та дані, що взаємодіють через переміщення (Variables and Data Interacting with Move)

Кілька змінних можуть взаємодіяти з тими самими даними по-різному в Rust. У Лістингу (Listing) 4-2 показано приклад з використанням цілого числа.

fn main() {
    let x = 5;
    let y = x;
}

Ми, ймовірно, можемо здогадатися, що тут відбувається: “Прив’язати значення 5 до x; потім зробити копію значення в x і прив’язати її до y.” Тепер у нас є дві змінні, x і y, і обидві дорівнюють 5. Саме це й відбувається, тому що цілі числа — це прості значення з відомим, фіксованим розміром, і обидва ці значення 5 проштовхуються у стек.

Тепер давайте подивимося на версію з String:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

Це виглядає дуже схоже, тож ми можемо припустити, що спосіб роботи буде такий самий: тобто другий рядок зробить копію значення в s1 і прив’яже її до s2. Але відбувається не зовсім це.

Подивіться на Figure 4-1, щоб побачити, що відбувається з String під капотом. String складається з трьох частин, показаних ліворуч: вказівника на пам’ять, що містить вміст рядка, довжини та ємності. Ця група даних зберігається у стеку. Праворуч — пам’ять у купі, яка містить вміст.

Two tables: the first table contains the representation of s1 on the
stack, consisting of its length (5), capacity (5), and a pointer to the first
value in the second table. The second table contains the representation of the
string data on the heap, byte by byte.

Рисунок (Figure) 4-1: Представлення в пам’яті String, що містить значення "hello", прив’язане до s1 (The representation in memory of a String holding the value "hello" bound to s1)

Довжина — це те, скільки пам’яті, у байтах, зараз використовує вміст String. Ємність — це загальний обсяг пам’яті, у байтах, який String отримав від алокатора. Різниця між довжиною та ємністю має значення, але не в цьому контексті, тож поки що її можна ігнорувати.

Коли ми присвоюємо s1 до s2, дані String копіюються, тобто ми копіюємо вказівник, довжину та ємність, що знаходяться у стеку. Ми не копіюємо дані в купі, на які вказує вказівник. Іншими словами, представлення даних у пам’яті виглядає як на Figure 4-2.

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.

Рисунок (Figure) 4-2: Представлення в пам’яті змінної s2, що має копію вказівника, довжини та ємності s1 (The representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1)

Представлення не виглядає як Figure 4-3, яке було б у пам’яті, якби Rust також копіював дані в купі. Якби Rust робив це, операція s2 = s1 могла б бути дуже дорогою з точки зору продуктивності під час виконання, якщо дані в купі були б великими.

Four tables: two tables representing the stack data for s1 and s2,
and each points to its own copy of string data on the heap.

Рисунок (Figure) 4-3: Інша можливість того, що могла б зробити s2 = s1, якби Rust також копіював дані в купі (Another possibility for what s2 = s1 might do if Rust copied the heap data as well)

Раніше ми сказали, що коли змінна виходить за межі області видимості, Rust автоматично викликає функцію drop і очищає пам’ять у купі для цієї змінної. Але Figure 4-2 показує, що обидва вказівники даних вказують на одне й те саме місце. Це проблема: коли s2 і s1 вийдуть за межі області видимості, вони обидва спробують звільнити ту саму пам’ять. Це відоме як помилка подвійного звільнення (double free) і є однією з помилок безпеки пам’яті, про які ми згадували раніше. Подвійне звільнення пам’яті (double free) може призвести до пошкодження пам’яті, що потенційно може призвести до вразливостей безпеки.

Щоб забезпечити безпеку пам’яті, після рядка let s2 = s1;, Rust вважає s1 більш не дійсною. Тому Rust не потрібно нічого звільняти, коли s1 виходить за межі області видимості. Подивіться, що відбувається, коли ви намагаєтеся використати s1 після створення s2; це не спрацює:

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

    println!("{s1}, world!");
}

Ви отримаєте таку помилку, тому що Rust не дає вам використовувати недійсне посилання:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |                ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Якщо ви чули терміни shallow copy і deep copy, працюючи з іншими мовами, концепція копіювання вказівника, довжини та ємності без копіювання даних, імовірно, звучить як створення shallow copy. Але оскільки Rust також робить першу змінну недійсною, замість того щоб це називалося shallow copy, це відоме як переміщення. У цьому прикладі ми б сказали, що s1 було переміщено у s2. Отже, те, що насправді відбувається, показано на Figure 4-4.

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.
Table s1 is grayed out because s1 is no longer valid; only s2 can be used to
access the heap data.

Рисунок (Figure) 4-4: Представлення в пам’яті після того, як s1 було анульовано (The representation in memory after s1 has been invalidated)

Це вирішує нашу проблему! Оскільки дійсною лишається лише s2, коли вона вийде за межі області видимості, вона одна звільнить пам’ять, і на цьому все.

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

Область видимості та присвоєння (Variables and Data Interacting with Move)

Інверсія цього твердження також справедлива для взаємозв’язку між областю видимості, володінням та звільненням пам’яті через функцію drop. Коли ви присвоюєте зовсім нове значення існуючій змінній, Rust викличе drop і негайно звільнить пам’ять початкового значення. Розглянемо, наприклад, цей код:

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

    println!("{s}, world!");
}

Спочатку ми оголошуємо змінну s і прив’язуємо її до String зі значенням "hello". Потім ми негайно створюємо новий String зі значенням "ahoy" і присвоюємо його s. У цей момент жоден об’єкт взагалі не посилається на початкове значення в купі. Figure 4-5 ілюструє дані стеку та купи тепер:

One table representing the string value on the stack, pointing to
the second piece of string data (ahoy) on the heap, with the original string
data (hello) grayed out because it cannot be accessed anymore.

Рисунок (Figure) 4-5: Представлення в пам’яті після того, як початкове значення було повністю замінено (The representation in memory after the initial value has been replaced in its entirety)

Отже, початковий рядок одразу виходить за межі області видимості. Rust запустить на ньому функцію drop, і його пам’ять буде звільнено негайно. Коли ми надрукуємо значення в кінці, це буде "ahoy, world!".

Змінні та дані, що взаємодіють через clone (Variables and Data Interacting with Clone)

Якщо ми дійсно хочемо глибоко скопіювати дані String у купі, а не лише дані зі стеку, ми можемо використати поширений метод під назвою clone. Синтаксис методів ми обговоримо в Розділі 5, але оскільки методи є поширеною можливістю в багатьох мовах програмування, ви, ймовірно, бачили їх раніше.

Ось приклад методу clone у дії:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");
}

Це працює цілком добре і явно створює поведінку, показану на Рисунку (Figure) 4-3, де дані в купі дійсно копіюються.

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

Дані лише у стеку: Copy (Stack-Only Data: Copy)

Є ще одна складність, про яку ми ще не говорили. Цей код із цілими числами — частина якого була показана в Лістингу (Listing) 4-2 — працює і є дійсним:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {x}, y = {y}");
}

Але цей код, здається, суперечить тому, що ми щойно дізналися: у нас немає виклику clone, але x усе ще дійсна і не була переміщена в y.

Причина в тому, що типи на кшталт цілих чисел, які мають відомий розмір під час компіляції, повністю зберігаються у стеку, тож копії фактичних значень створюються швидко. Це означає, що немає жодної причини перешкоджати тому, щоб x залишалася дійсною після створення змінної y. Іншими словами, тут немає різниці між глибоким і неглибоким копіюванням, тож виклик clone не дав би нічого іншого порівняно зі звичайним неглибоким копіюванням, і ми можемо його не вживати.

Rust має спеціальну анотацію під назвою трейту (trait) Copy, яку ми можемо застосовувати до типів, що зберігаються у стеку, як-от цілі числа (ми ще поговоримо більше про трейти (traits) у Розділі 10). Якщо тип реалізує трейт (trait) Copy, змінні, які його використовують, не переміщуються, а просто копіюються, залишаючись дійсними після присвоєння іншій змінній.

Rust не дозволить нам анотувати тип Copy, якщо тип або будь-яка з його частин реалізували трейт (trait) Drop. Якщо для типу має відбуватися щось спеціальне, коли значення виходить за межі області видимості, і ми додаємо до цього типу анотацію Copy, ми отримаємо помилку під час компіляції. Щоб дізнатися, як додати анотацію Copy до вашого типу, щоб реалізувати трейт, див. “Derivable Traits” в Додатку C.

Отже, які типи реалізують трейт Copy? Ви можете перевірити документацію для певного типу, щоб переконатися, але загальне правило таке: будь-яка група простих скалярних значень може реалізувати Copy, а все, що потребує виділення пам’яті або є будь-якою формою ресурсу, не може реалізувати Copy. Ось деякі з типів, які реалізують Copy:

  • Усі цілі типи, як-от u32.
  • Булевий тип, bool, зі значеннями true і false.
  • Усі типи чисел із рухомою комою, як-от f64.
  • Символьний тип, char.
  • Кортежі, якщо вони містять лише типи, які також реалізують Copy. Наприклад, (i32, i32) реалізує Copy, але (i32, String) — ні.

Володіння / Власність (Ownership) і функції

Механіка передавання значення у функцію подібна до тієї, що й під час присвоєння значення змінній. Передавання змінної у функцію перемістить або скопіює її, так само як і присвоєння. У Лістингу (Listing) 4-3 є приклад із деякими позначками, що показують, де змінні входять у область видимості та виходять з неї.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // Because i32 implements the Copy trait,
                                    // x does NOT move into the function,
                                    // so it's okay to use x afterward.

} // Here, x goes out of scope, then s. However, because s's value was moved,
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.

Якби ми спробували використати s після виклику takes_ownership, Rust викинув би помилку під час компіляції. Ці статичні перевірки захищають нас від помилок. Спробуйте додати код до main, який використовує s і x, щоб побачити, де ви можете їх використовувати і де правила володіння (ownership) не дозволяють вам це робити.

Значення, що повертаються, і область видимості (Return Values and Scope)

Повернення значень також може передавати володіння (ownership). У Лістингу (Listing) 4-4 показано приклад функції, яка повертає деяке значення, з такими ж позначками, як і в Лістингу (Listing) 4-3.

fn main() {
    let s1 = gives_ownership();        // gives_ownership moves its return
                                       // value into s1

    let s2 = String::from("hello");    // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
                                       // takes_and_gives_back, which also
                                       // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {       // gives_ownership will move its
                                       // return value into the function
                                       // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                        // some_string is returned and
                                       // moves out to the calling
                                       // function
}

// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string comes into
    // scope

    a_string  // a_string is returned and moves out to the calling function
}

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

Хоча це працює, брати володіння (ownership) і потім повертати володіння (ownership) у кожній функції трохи виснажливо. Що якби ми хотіли дозволити функції використати значення, але не брати володіння? Досить незручно, що все, що ми передаємо, також потрібно передати назад, якщо ми хочемо використати це знову, окрім будь-яких даних, що виникають у тілі функції, які ми також можемо захотіти повернути.

Rust справді дозволяє повертати кілька значень за допомогою кортежу, як показано в Лістингу (Listing) 4-5.

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

Але це забагато церемонності й багато роботи для поняття, яке має бути звичним. На щастя для нас, у Rust є можливість використовувати значення без передавання володіння (ownership): посилання.