Ownership i Borrowing у Rust: повний посібник з прикладами коду

Детальний розбір ownership та borrowing у Rust з практичними прикладами. Семантика переміщення, посилання, часи життя та borrow checker для безпечного керування пам'яттю.

Rust ownership and borrowing memory management visualization

Ownership та borrowing становлять основу гарантій безпеки пам'яті в Rust. На відміну від мов зі збиранням сміття, Rust застосовує суворі правила на етапі компіляції через borrow checker, що усуває цілі класи помилок -- розіменування нульових вказівників, гонки даних та використання після звільнення -- без жодних витрат під час виконання.

Три правила ownership

Кожне значення в Rust має рівно одного власника. Коли власник виходить з області видимості, значення знищується. Право власності можна передати (перемістити) або тимчасово надати в користування (позичити). Ці три правила повністю замінюють збирання сміття.

Як семантика переміщення замінює збирання сміття

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

move_semantics.rsrust
fn main() {
    let original = String::from("interview prep");
    let moved = original; // ownership transfers here

    // println!("{}", original); // compile error: value moved
    println!("{}", moved); // works fine
}

Такий підхід запобігає помилкам подвійного звільнення. Тип String виділяє пам'ять у купі, тому Rust гарантує, що лише одна змінна володіє цим виділенням у будь-який момент часу. Типи, що зберігаються виключно на стеку, такі як i32 або bool, реалізують трейт Copy і дублюються замість переміщення.

copy_vs_move.rsrust
fn main() {
    let x: i32 = 42;
    let y = x; // copy, not move -- i32 is Copy
    println!("x = {}, y = {}", x, y); // both valid

    let s1 = String::from("hello");
    let s2 = s1.clone(); // explicit deep copy
    println!("s1 = {}, s2 = {}", s1, s2); // both valid after clone
}

Різниця між Copy та Clone має значення на співбесідах: Copy -- неявний і дешевий (побітове копіювання), тоді як Clone -- явний і може бути дорогим (виділення пам'яті в купі).

Запозичення через незмінні посилання

Передача права власності скрізь зробила б код непрактичним. Borrowing у Rust вирішує цю проблему, надаючи доступ до значення без передачі права власності. Незмінне посилання (&T) дозволяє доступ лише для читання, і кілька незмінних посилань можуть існувати одночасно.

immutable_borrowing.rsrust
fn calculate_length(s: &String) -> usize {
    s.len() // read access through the reference
} // s goes out of scope, but doesn't drop the String (not the owner)

fn main() {
    let greeting = String::from("hello, Rust");
    let len = calculate_length(&greeting); // borrow, don't move
    println!("'{}' has {} characters", greeting, len); // greeting still valid
}

Символ & створює посилання, яке запозичує значення. Сигнатура функції &String оголошує, що calculate_length запозичує без отримання права власності. Після повернення функції викликаючий код зберігає повне право власності.

Правила запозичення

У будь-який момент часу значення може мати або: багато незмінних посилань (&T), АБО рівно одне змінне посилання (&mut T). Ніколи обидва одночасно. Це правило запобігає гонкам даних на етапі компіляції.

Змінні посилання та правило ексклюзивності

Змінні посилання (&mut T) надають доступ для запису, але вимагають ексклюзивності: лише одне змінне посилання на значення може існувати в заданій області видимості. Це запобігає одночасній модифікації одних і тих самих даних двома фрагментами коду.

mutable_borrowing.rsrust
fn append_greeting(s: &mut String) {
    s.push_str(", welcome to Rust!"); // modify through mutable ref
}

fn main() {
    let mut message = String::from("Hello");
    append_greeting(&mut message);
    println!("{}", message); // "Hello, welcome to Rust!"
}

Ключове слово mut з'являється у трьох місцях: прив'язка змінної (let mut), тип посилання (&mut) та параметр функції. Усі три є обов'язковими. Спроба створити друге змінне посилання в тій самій області видимості спричиняє помилку компіляції.

exclusivity_rule.rsrust
fn main() {
    let mut data = String::from("shared state");

    let r1 = &mut data;
    // let r2 = &mut data; // compile error: second mutable borrow
    println!("{}", r1);

    // After r1's last usage, a new mutable borrow is allowed
    let r3 = &mut data; // this works -- non-lexical lifetimes
    r3.push_str(" updated");
    println!("{}", r3);
}

Редакція Rust 2021 використовує нелексичні часи життя (NLL): запозичення завершується в точці останнього використання, а не в кінці блоку області видимості. Це робить правило ексклюзивності більш ергономічним без втрати безпеки.

Готовий до співбесід з Rust?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Часи життя: як пояснити компілятору тривалість існування посилань

Часи життя (lifetimes) -- це спосіб Rust гарантувати, що посилання ніколи не переживуть дані, на які вони вказують. Здебільшого компілятор виводить часи життя автоматично через правила виведення часів життя. Явні анотації стають необхідними, коли взаємодіють кілька посилань.

lifetime_annotation.rsrust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let result;
    let string1 = String::from("Rust ownership");
    {
        let string2 = String::from("borrowing");
        result = longest(string1.as_str(), string2.as_str());
        println!("Longest: {}", result);
    }
}

Синтаксис 'a -- це параметр часу життя, а не нова концепція. Він анотує зв'язки, які вже існують. Сигнатура функції стверджує: "вихідне посилання не може пережити жодне з вхідних посилань". Компілятор використовує це для запобігання висячим посиланням.

Запозичення у структурах та обмеження часів життя

Структури, що містять посилання, повинні оголошувати параметри часів життя. Це гарантує, що структура не може пережити дані, на які посилається -- поширене джерело висячих вказівників у C/C++.

struct_lifetime.rsrust
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn summary(&self) -> &str {
        let end = self.text.len().min(20);
        &self.text[..end]
    }
}

fn main() {
    let article = String::from("Rust ownership model eliminates memory bugs");
    let excerpt = Excerpt {
        text: article.as_str(),
    };
    println!("Summary: {}", excerpt.summary());
}

Час життя 'a у Excerpt<'a> прив'язує валідність структури до базового рядка. Знищення article до excerpt спричинить помилку компіляції.

Типова пастка на співбесіді

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

Патерни ownership у реальному коді на Rust

Продакшн-код на Rust базується на кількох повторюваних патернах ownership. Розпізнавання цих патернів прискорює як розробку, так і підготовку до співбесід.

ownership_patterns.rsrust
// Pattern 1: Take ownership, return a new value
fn process_and_return(mut input: String) -> String {
    input.push_str(" -- processed");
    input
}

// Pattern 2: Borrow for read-only inspection
fn contains_keyword(text: &str, keyword: &str) -> bool {
    text.to_lowercase().contains(&keyword.to_lowercase())
}

// Pattern 3: Borrow mutably for in-place modification
fn sanitize(input: &mut String) {
    *input = input.trim().to_string();
}

fn main() {
    let raw = String::from("user input");
    let processed = process_and_return(raw);

    let found = contains_keyword(&processed, "input");
    println!("Contains 'input': {}", found);

    let mut padded = String::from("  spaces everywhere  ");
    sanitize(&mut padded);
    println!("Sanitized: '{}'", padded);
}

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

Помилки borrow checker та способи їх виправлення

Borrow checker генерує специфічні коди помилок. Розуміння найпоширеніших з них перетворює роздратовуючі помилки компіляції на прості виправлення.

common_fixes.rsrust
fn main() {
    let mut scores = vec![90, 85, 78];
    let first = scores[0];
    scores.push(95);
    println!("First: {}, All: {:?}", first, scores);

    let name = String::from("Alice");
    let greeting = format!("Hello, {}", name);
    println!("{} says {}", name, greeting);

    let outer;
    {
        let inner = String::from("temporary");
        outer = inner;
    }
    println!("{}", outer);
}

Кожне виправлення слідує одному принципу: реструктуризувати код так, щоб запозичення та право власності відповідали правилам Rust. Боротьба з borrow checker зазвичай сигналізує про проблему проектування, яка спричинила б помилки в інших мовах. Для більш складних патернів, пов'язаних з конкурентністю та запозиченням, типи спільного володіння на кшталт Arc та Mutex стають незамінними.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Висновок

  • Кожне значення в Rust має одного власника; право власності передається при присвоєнні (семантика переміщення), якщо тип не реалізує Copy
  • Незмінні посилання (&T) дозволяють спільний доступ для читання; змінні посилання (&mut T) забезпечують ексклюзивний доступ для запису
  • Borrow checker запобігає гонкам даних та висячим посиланням на етапі компіляції з нульовими витратами під час виконання
  • Часи життя анотують зв'язки між посиланнями -- вони описують існуючі обмеження, а не створюють нові
  • Коли borrow checker відхиляє код, слід реструктуризувати потік ownership замість використання unsafe
  • Практика цих патернів з питаннями для співбесід з Rust допоможе набути впевненості перед технічними співбесідами

Теги

#rust
#ownership
#borrowing
#memory-management
#systems-programming

Поділитися

Пов'язані статті