Borrow мать его checker

#rust

Rust известен своей безопасностью, все кричат про type safe и получают оргазм, это действительно так (я не про оргазм). Но у всего есть цена, и название ему borrow checker

Thanos meme

Звучит как нечто плохое, что конечно же не так. Это другой способ, сильно другой.

Если вы переходите на Rust с другого языка, например, с++, python, java (фу боже) js/ts или сексуальный php, вас точно удивит, а кого то раздражать концепция Borrow Checker. Начнем с предусловия, погнали -->

Что это?

Borrow checker часть компилятора Rust, он следит за тем, как ваш код использует ссылки (borrows) на данные. Чекает что все операции с данными в вашем коде соответствуют правилам владения ownership (про это будет чуть ниже).

Зачем? для гарантии, что в коде не возникнут проблемы с памятью, такие как висячие указатели, гонки данных или неопределенное поведение. Если вы привыкли к c/c++, где подобное приходится ловить руками, borrow checker'у есть чем вас удивить.

Это важно, в c/c++ вы управляете памятью руками. Это клево, но легко прострелить себе ногу, особенно когда софт большой. В более зумерских языках, python, java (фу боже), js/ts или сексуальный php, памятью управляет сборщик мусора (GC), это удобно и не нужно думать, но это накладные расходы.

Rust предлагает другое, контроль памяти без GC и без ручного управления, но через строгие правила времени компиляции.

Прежде чем углубляться в работу borrow checker, давайте разберем три вещи, это:

  • ownership (владение)
  • borrwoing (ссылки)
  • lifetimes (время жизни)

Погнали -->

Ownership

Каждое значение в Rust имеет владельца (owner) переменную, которая отвечает за его удаление. Когда владелец выходит из области видимости, значение удаляется (вызывается drop). Го пример -->

fn main() {
    let a = String::from("JVM SUCKS"); // a владелец строки (owner)
    take_ownership(a); // a передаем в функцию, владение переходит к ней

    // тут ошибка, a больше не владеет данными
    // println!("{}", a); 
}

fn take_ownership(a: String) {
    println!("{}", a);
} // здесь a выходит из области видимости, память освобождается

Аналог из плюсов: похож на std::unique_ptr который нельзя скопировать, но можно переместить

Borrowing

Что бы избежать постоянной передачи владения, Rust позволяет брать ссылки на данные (грубо говоря одолжить)

Неизменяемая ссылка &T (можно читать данные, но нельзя изменять. может быть сколько угодно)

Изменяемая ссылка &mut T (можно изменять данные. только одна активная ссылка в области видимости)

fn main() {
    let mut s = String::from("php sexy");
    let r1 = &s; // неизменяемая ссылка
    let r2 = &s; // еще одна неизменяемая
    println!("{} and {}", r1, r2); // тут всё гуд

    let r3 = &mut s; // изменяемая ссылка
    r3.push_str(" world!");

    // будет ошибка, нельзя использовать r1, пока активна r3
    // println!("{}", r1); 
}

Правила заимствования:

  • Либо одна изменяемая ссылка, либо сколько угодно неизменяемых
  • Ссылки не должны «жить» дольше владельца

Lifetimes

Borrow Checker проверяет, что все ссылки действительны в течение своего времени жизни. Обычно компилятор выводит lifetimes автоматически, но иногда их нужно указывать явно:

// пример явного указания времени жизни
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

Здесь 'a означает, что возвращаемая ссылка живет столько же, сколько и меньшая из s1 или s2

Вы привыкните

Понимаю, не привычно, но привыкнуть не сложно (даже если кажется что нет) Практика и еще раз практика. Через пару недель кодинга более менее станет интуитивным.

p.s если застряли, поищите паттерны типа «передачи владения через структуры» или использования умных указателей (Rc, Arc, RefCell). Иногда спорить с компилятором rust это дефолт, xd

Бонус, держите tips

Изменяйте данные только после того, как ссылки выйдут из области видимости.

let mut x = 5;
let y = &x;
x = 10; // будет ошибка
// нельзя изменять x, пока есть активная ссылка y

println!("{}", y);

Го дальше -->

Висячая ссылка

fn dangle() -> &String {
    let s = String::from("Hello");
    &s // будет ошибка! 
    // s удаляется при выходе из функции, ссылки нету
}

Верните владение вместо ссылки:

fn no_dangle() -> String {
    let s = String::from("Hello");
    s // владение передается наружу
}

Го дальше -->

let mut s = String::new();
let r1 = &mut s;
let r2 = &mut s; // будет ошибка 
// две изменяемых ссылки на s

r1.push_str("test");

Используйте ссылки в разных областях видимости -->

let mut s = String::new();
{
    let r1 = &mut s;
    r1.push_str("test");
} // r1 выходит из области видимости

let r2 = &mut s; // теперь можно

Эти псевдопримеры можно приводить бесконечно

  • Начните с неизменяемых ссылок. используйте & везде, где возможно
  • Локализуйте изменяемость. чем меньше кода имеет доступ к изменяемым данным, тем проще
  • Доверяйте компилятору. сообщения об ошибках лучший учебник, читайте как библию
  • Используйте clone() как временное решение, если зашли в тупик (но сильно не абузьте)

И еще раз, практика и практика, благо у rust активное сообщество и оно с каждым днем растет, заглядывайте в доку, гуглите, читайте книги.