Borrow мать его checker
Rust известен своей безопасностью, все кричат про type safe и получают оргазм, это действительно так (я не про оргазм). Но у всего есть цена, и название ему borrow checker
Звучит как нечто плохое, что конечно же не так. Это другой способ, сильно другой.
Если вы переходите на 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 активное сообщество и оно с каждым днем растет, заглядывайте в доку, гуглите, читайте книги.