lenec ru

← все посты

Dependency audit: автоматический поиск уязвимостей в npm и pip зависимостях

12K

Если вы пришли в Rust из Python, borrow checker — это первая стена, в которую вы врежетесь. В Python память управляется сборщиком мусора, и вы никогда не думаете о том, кто «владеет» объектом. В Rust владение — центральная концепция, которая гарантирует безопасность памяти без GC. Разберём ownership через призму Python-мышления — с аналогиями, примерами и разбором типичных ошибок.

Зачем ownership: memory safety без GC

Python использует reference counting + циклический GC. Это удобно, но стоит дорого: непредсказуемые паузы, overhead на каждый объект, невозможность гарантировать отсутствие data races в многопоточном коде (отсюда GIL).

Rust выбрал другой путь: компилятор статически проверяет, что в каждый момент времени у данных ровно один владелец. Когда владелец выходит из scope — память освобождается. Нет GC, нет runtime-overhead, нет data races. Цена — вы должны убедить компилятор, что ваш код безопасен.

Аналогии с Python: ярлыки vs владение

В Python переменные — это ярлыки (имена), привязанные к объектам. Несколько ярлыков могут указывать на один объект:

# Python: a и b указывают на один и тот же список
a = [1, 2, 3]
b = a          # b — ещё один ярлык на тот же объект
b.append(4)    # мутация видна через оба имени
print(a)       # [1, 2, 3, 4]

В Rust переменная — это владелец значения. Присваивание не создаёт второй ярлык, а передаёт владение (move):

fn main() {
    let a = vec![1, 2, 3];
    let b = a;          // ownership перешёл к b, a больше недействителен
    // println!("{:?}", a); // ОШИБКА: value used after move
    println!("{:?}", b);   // [1, 2, 3]
}

Это ключевое отличие: в Python объект живёт, пока на него есть хоть один ярлык. В Rust значение живёт, пока жив его единственный владелец.

Move, Clone, Copy — когда что происходит

Move — передача владения. Происходит по умолчанию для типов, хранящих данные в куче (String, Vec, Box):

let s1 = String::from("hello");
let s2 = s1;    // move: s1 больше недействителен

Clone — явное глубокое копирование. Дорого, но создаёт независимую копию:

let s1 = String::from("hello");
let s2 = s1.clone();  // глубокая копия, оба действительны
println!("{} {}", s1, s2); // hello hello

Copy — автоматическое побитовое копирование для простых типов на стеке (i32, f64, bool, char). Аналог того, как Python копирует int:

let x: i32 = 42;
let y = x;     // Copy: x по-прежнему действителен
println!("{} {}", x, y); // 42 42

Правило: если тип реализует трейт Copy — присваивание копирует. Иначе — перемещает.

References и borrowing: &T vs &mut T

Чтобы не передавать владение каждый раз, Rust позволяет «одолжить» значение через ссылки:

fn print_length(s: &String) {  // заимствование: читаем, не владеем
    println!("Length: {}", s.len());
}

fn add_exclaim(s: &mut String) {  // мутабельное заимствование
    s.push('!');
}

fn main() {
    let mut greeting = String::from("Hello");
    print_length(&greeting);      // иммутабельная ссылка
    add_exclaim(&mut greeting);   // мутабельная ссылка
    println!("{}", greeting);      // Hello!
}

Правила borrowing (компилятор проверяет статически):

  • Можно иметь сколько угодно иммутабельных ссылок &T одновременно
  • Можно иметь ровно одну мутабельную ссылку &mut T
  • Нельзя иметь &T и &mut T одновременно

Аналогия из Python: представьте, что &T — это read lock, а &mut T — write lock. Rust применяет эту дисциплину на этапе компиляции, а не в рантайме.

Lifetime annotations: когда компилятор не справляется

Обычно Rust выводит lifetimes автоматически. Но когда функция возвращает ссылку, компилятору нужна подсказка — как долго эта ссылка действительна:

// Без аннотации — ошибка компиляции
// fn longest(a: &str, b: &str) -> &str { ... }

// С аннотацией: результат живёт столько же, сколько кратчайший из входов
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("hi");
        result = longest(&s1, &s2);
        println!("{}", result); // OK: s2 ещё жив
    }
    // println!("{}", result); // ОШИБКА: s2 уже уничтожен
}

Lifetime 'a — это не время жизни, а ограничение: «результат не переживёт ни один из входов». Компилятор использует эту информацию, чтобы предотвратить dangling references.

Типичные ошибки новичков и как их читать

1. «value used after move» — вы использовали переменную после передачи владения. Решение: используйте .clone() или передавайте ссылку.

2. «cannot borrow as mutable because it is also borrowed as immutable» — одновременно существуют &T и &mut T. Решение: завершите использование иммутабельной ссылки до мутабельного заимствования.

3. «does not live long enough» — ссылка переживает данные, на которые указывает. Решение: измените scope переменной или верните owned-значение вместо ссылки.

4. «cannot move out of borrowed content» — пытаетесь забрать владение через ссылку. Решение: используйте .clone() или перестройте логику.

Главный совет: читайте ошибки компилятора целиком. Rust выдаёт подробные объяснения с подсказками. Через 2-3 недели активной работы borrow checker перестаёт быть врагом и становится помощником — он ловит баги, которые в Python проявились бы только в продакшене.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.