☰ Конспект ← Курс
ЛЕКЦІЯ 5

ВОЛОДІННЯ (OWNERSHIP)

Хто власник значення

VTFK · 2026

План лекції

  • Три правила володіння
  • Стек, купа і типи з Copy-семантикою
  • Move — переміщення власності
  • Clone — явна глибока копія
  • Володіння у функціях: передача і повернення
  • Живе демо: помилка E0382

Фокус: одне значення — один власник, завжди

Три правила володіння

  1. 1
    У кожного значення є власникзмінна, якій значення належить (owner)
  2. 2
    Власник у кожен момент — одиндвох власників одного значення не буває
  3. 3
    Вихід з області видимості — dropкомпілятор вставляє звільнення сам, детерміновано

Ці три правила компілятор перевіряє статично, без збирача сміття в рантаймі

Стек, копіювання і типи з Copy

rust
let x = 3101;       // 0xC1D, i32 — живе на стеку
let y = x;          // копія: обидва x і y валідні
println!("{} {}", x, y);   // працює: i32 реалізує трейт Copy

// прості типи фіксованого розміру на стеку (i32, bool, char, f64, кортежі з них)
// копіюються автоматично — тут немає move

Copy-типи — виняток із загального правила move

Що таке move

  • Для типів на купі (String, Vec, Box<T>) присвоєння не копіює дані — переміщує власність
  • Стара змінна після move недійсна: компілятор більше не дозволить її використати
  • Це запобігає double free: звільняти двічі нема кому — власник один
СТЕК (швидко, LIFO) КУПА (heap) let s = String ptr,len,cap x: i32 = 5 на стеку "HELLO" дані рядка let s2 = s; // MOVE s — недійсний moved out s2 → "HELLO" один власник рівно один власник даних у купі → немає подвійного звільнення

Move: компілятор забороняє повторне використання

rust
let s1 = String::from("hello");
let s2 = s1;               // власність переходить від s1 до s2

println!("{}", s1);       // error[E0382]: value borrowed here after move
// ^ s1 більше не власник — компілятор це знає і не дає скомпілювати

error[E0382]: use of moved value

Що відбувається при `let s2 = s1;`

MOVE: ПЕРЕЇЖДЖАЄ ЗАГОЛОВОК, А НЕ ДАНІ СТЕК · let s2 = s1; s1 ptr len cap moved out E0382 при вживанні s2 ptr → купа len 5 cap 5 єдиний власник він і викличе drop 24 Б КУПА · буфер String String::from("hello") h e l l o буфер не рухається 0 байтів даних скопійовано ВИНЯТОК · COPY-ТИПИ let x = 3101; let y = x; 3101 x 3101 y на купі нічого нема — копіюється все обидва лишаються валідними власник один; move — це передача відповідальності за drop, а не копія даних

Копіюється лише заголовок (ptr/len/cap = 24 байти); буфер "hello" на купі не рухається, а s1 стає недійсним. Copy-типи на кшталт i32 — виняток: у них на купі нічого нема

Move проти Clone

Move — типова поведінка
  • Копіює лише заголовок: вказівник, довжина, місткість
  • Буфер на купі не чіпається
  • Стара змінна стає недійсною
  • Дешево: не залежить від розміру даних
  • Синтаксис — звичайне присвоєння =
vs
Clone — явна дія
  • Виділяє нову ділянку на купі
  • Копіює вміст байт за байтом
  • Обидві змінні — незалежні власники
  • Дорого: пропорційно розміру даних
  • Синтаксис — видимий виклик .clone()

Rust робить дешеву операцію типовою, а дорогу — явною: читач бачить кожну алокацію

Що насправді переміщує move у String

24
байти копіюються при move
заголовок: ptr + len + capacity на 64-бітній системі
0
байтів буфера на купі
дані не рухаються — переходить лише право на них
1
власник у кожен момент
стара змінна недійсна після move
1
виклик drop на значення
double free структурно неможливий

Move не пропорційний розміру даних — саме тому він може бути типовою поведінкою

Clone на практиці

rust
let s1 = String::from("hello");
let s2 = s1.clone();       // глибока копія: нова ділянка пам'яті

println!("{} {}", s1, s2); // ОК: обидві змінні валідні й незалежні

Clone видно в коді — це свідомий вибір, а не прихована вартість

Власність у виклику функції

s1 у викликачаString::from("дані")
move у функціюtakes_ownership(s1) — s1 недійсний
s усерединіфункція — власник на час виклику
return sвласність повертається результатом
s1 знову власникlet s1 = takes_ownership(s1)

Передавати й повертати власність через кожну функцію незручно — тому в Л6 з'явиться запозичення

Володіння переходить у функцію і повертається

rust
fn takes_ownership(s: String) -> String {
    println!("отримав: {}", s);
    s   // повертаємо власність назад викликачу
}

let s1 = String::from("дані");
let s1 = takes_ownership(s1);   // власність пішла у функцію й повернулась
println!("{}", s1);            // s1 знову валідний

Постійно передавати й повертати власність незручно — саме тому в Л6 з'явиться запозичення (borrowing)

Живе демо

  1. Пишемо функцію, яка приймає String за значенням і нічого не повертає
  2. Викликаємо її з s1, потім намагаємось знову вивести s1 через println!
  3. Запускаємо `cargo build` і читаємо повідомлення компілятора error[E0382]
  4. Виправляємо, повертаючи власність з функції (як на попередньому слайді), переконуємось, що збирається

Очікувано: Перша версія не компілюється з чіткою вказівкою рядка й причини; виправлена версія збирається і виконується

Copy чи move? Залежить від типу

ТипCopy?Присвоєння let b = aЧому
i32, f64, bool, charтакпобітова копія, a валіднефіксований розмір, повністю на стеку
кортеж (i32, f64)такпобітова копіяусі елементи — Copy
масив [u8; 4]такпобітова копіяусі елементи — Copy
String, Vec<T>, Box<T>ніmove, a недійсневолодіють буфером на купі
struct з полем Stringніmoveхоч одне поле не Copy
struct з #[derive(Copy)]такпобітова копіяусі поля Copy і немає Drop

Тип не може одночасно реалізовувати Copy і мати власну реалізацію Drop

Copy проти Clone

ОзнакаCopyClone
Викликаєтьсянеявно, при присвоєнніявно, методом .clone()
Вартістьзавжди дешева: побітова копіябудь-яка: глибоке копіювання
Хто може реалізуватитипи без ресурсів на купі й без Dropпрактично будь-який тип
Прикладиi32, bool, (f64, f64)String, Vec<T>, більшість структур
Зв'язокCopy вимагає CloneClone не вимагає Copy

`#[derive(Clone, Copy)]` працює, лише якщо всі поля структури самі реалізують ці трейти

derive(Clone, Copy) для власної структури

rust
#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1;              // Copy: побітова копія, p1 і далі валідний
println!("{:?} {:?}", p1, p2);

derive працює, лише якщо всі поля структури самі реалізують Copy

Часткове переміщення полів структури

  • Move працює на рівні окремих полів: перемістити поле — не перемістити структуру
  • Непереміщені поля лишаються доступними після часткового переміщення
  • Структуру цілком (наприклад, через {:?}) використати не можна, якщо поле переміщене
  • Компілятор відстежує стан переміщення окремо для кожного поля

Часткове переміщення на практиці

rust
struct Runner {
    name: String,
    level: u32,   // u32 реалізує Copy
}

let r = Runner { name: String::from("ранер"), level: 3 };
let name = r.name;          // часткове переміщення поля name

println!("{}", r.level);    // ОК: level — Copy, не було переміщене
// println!("{}", r.name);  // error: r.name вже переміщено

Компілятор дозволяє доступ до непереміщених полів навіть після часткового move

Drop і RAII

🔚
Drop::drop автоматичновикликається, коли власник виходить з області видимості
🏗️
RAII як у C++але примусово перевірений компілятором
🔁
Зворотний порядокостаннє створене знищується першим — навіть під час паніки
🚫
x.drop() забороненораннє звільнення — лише std::mem::drop(x)
🔌
Ресурси, не тільки пам'ятьфайли, з'єднання, м'ютекси звільняються детерміновано
1️⃣
Рівно один викликне можна забути звільнити або звільнити двічі

Без збирача сміття і без ручного free()/close() у кожному місці виходу з функції

impl Drop приклад

rust
struct Guard {
    name: String,
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("звільняю ресурс: {}", self.name);
    }
}

fn main() {
    let _g = Guard { name: String::from("з'єднання") };
    println!("працюємо...");
}   // тут автоматично: "звільняю ресурс: з'єднання"

Звільнення ресурсу детерміноване й гарантоване компілятором, без ручного close()

Підводні камені

ПасткаЩо станетьсяЯк уникнути
Очікувати, що s2 = s1 копіює Stringце move: s1 стає недійсним.clone(), якщо потрібні дві копії
#[derive(Copy)] на структурі з Stringне скомпілюєтьсяCopy — лише коли всі поля Copy
Викликати x.drop() вручнузаборонено компіляторомstd::mem::drop(x)
Часткове переміщення, потім {:?} структуриerror: use of partially moved valueперемістити поле в останню чергу або .clone()
Повертати власність з функції, що лише читаєбагатослівний кодзапозичення &T — тема Л6
Володіння — найунікальніша риса Rust, і вона має глибокі наслідки для всієї мови: саме воно дає гарантії безпеки пам'яті без збирача сміття.

— The Rust Programming Language, розд. 4 «Understanding Ownership»

Підсумок

  • Кожне значення в Rust має рівно одного власника; вихід з області видимості — drop
  • Присвоєння типів на купі — це move, а не копіювання; стара змінна стає недійсною
  • Clone — явна, видима в коді операція глибокого копіювання
  • Постійна передача власності у функції й назад незручна — далі буде запозичення (Л6)

🥚 У прикладі Copy змінна x дорівнює 3101 — 0xC1D. Побітова копія такого i32 коштує рівно 4 байти; сама константа в матеріалах курсу з'являється не вперше.

VTFK · TERMLINK · L05← → · space · f — на весь екран