Dependency audit: автоматический поиск уязвимостей в npm и pip зависимостях
Если вы пришли в 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 проявились бы только в продакшене.