Что такое заимствование (borrowing) в Rust?
Заимствование (borrowing) в Rust — это механизм временного доступа к значению без передачи владения. Оно позволяет ссылаться на данные, находящиеся во владении другой переменной, при этом не нарушая строгих правил владения и не вызывая утечек или гонок данных. Заимствование — один из ключевых элементов системы безопасности Rust, которая обеспечивает управление памятью без сборщика мусора.
Почему в Rust нужно заимствование
Поскольку в Rust у каждого значения может быть только один владелец, то передача данных в функцию или переменной с перемещением (move) делает исходную переменную недоступной. Это не всегда удобно. Чтобы можно было использовать значение в разных местах без передачи полного владения, и существует механизм заимствования.
Например:
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn main() {
let my_string = String::from("Hello");
print_length(&my_string); // передаем ссылку, не ownership
println!("{}", my_string); // my_string все еще доступна
}
Здесь print_length получает доступ к строке, но не забирает её из main. Это возможно, потому что она работает с ссылкой, а не с самим значением.
Два вида заимствования
В Rust есть два типа заимствований: неизменяемое и изменяемое.
1. Неизменяемое заимствование (&T)
Позволяет другим частям кода читать значение без возможности его изменить. Допускается любое количество одновременно действующих неизменяемых заимствований, пока нет ни одного изменяемого.
Пример:
let s = String::from("Hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);
Важно: при наличии хотя бы одного неизменяемого заимствования нельзя создавать изменяемое:
let s = String::from("Hello");
let r1 = &s;
let r2 = &mut s; // ошибка компиляции!
2. Изменяемое заимствование (&mut T)
Позволяет изменить значение. Но можно создать только одно изменяемое заимствование в конкретный момент времени. Это правило защищает от состояний гонки и одновременного изменения значения из разных мест.
Пример:
let mut s = String::from("Hello");
let r = &mut s;
r.push_str(", world");
Пока существует изменяемая ссылка, нельзя создать ни одной другой ссылки (ни изменяемой, ни неизменяемой):
let mut s = String::from("Hello");
let r1 = &mut s;
let r2 = &s; // ошибка: нельзя смешивать &mut и &
Область действия заимствования
Rust строго следит за временем жизни ссылок. Заимствование действует с момента создания ссылки до момента последнего использования.
Пример:
let mut s = String::from("Rust");
let r1 = &s;
println!("{}", r1); // заимствование активно здесь
// r1 больше не используется, теперь можно создать изменяемую ссылку
let r2 = &mut s;
r2.push_str(" language");
Компилятор анализирует "живые" ссылки (живущие borrow) и запрещает перекрытие по типу & и &mut.
Почему такие ограничения?
Ограничения на одновременные ссылки существуют, чтобы гарантировать безопасность доступа к памяти. В других языках (например, C++) ничто не мешает двум указателям одновременно изменять одно и то же значение, что приводит к неопределенному поведению, утечкам, повреждениям данных и багам, которые тяжело отследить. Rust же выявляет такие потенциальные ошибки на этапе компиляции.
Заимствование и функции
Функции часто используют заимствование, чтобы работать с большими структурами данных без копирования.
fn modify(s: &mut String) {
s.push_str(" updated");
}
fn main() {
let mut text = String::from("Text");
modify(&mut text);
println!("{}", text);
}
Функция modify получает временный доступ к text, может изменить его, но не становится его владельцем.
Заимствование и время жизни (lifetimes)
Когда речь идет о сложных структурах, функций или ссылках, у которых время жизни неочевидно, Rust использует времена жизни — lifetimes — для описания того, как долго заимствованная ссылка должна быть допустимой. Например:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Здесь 'a — это параметр времени жизни, который указывает: возвращаемая ссылка живёт как минимум столько же, сколько самая короткая из x и y.
Внутренние структуры заимствования: RefCell и Borrow
Rust применяет заимствование не только в базовом синтаксисе, но и через структуры типа RefCell<T>. Это динамически проверяемое заимствование, в отличие от статически проверяемого обычного &T/&mut T.
Пример:
use std::cell::RefCell;
let data = RefCell::new(5);
\*data.borrow_mut() += 1;
RefCell позволяет обойти ограничения компилятора и нарушать правила заимствования во время выполнения (а не на этапе компиляции), но при этом гарантирует корректность: если вы нарушите правила (например, попытаетесь одновременно взять borrow_mut и borrow), программа упадёт с паникой.
Итого, как работает borrowing:
-
Заимствование — это временный доступ к данным.
-
Оно может быть неизменяемым (&T) или изменяемым (&mut T).
-
Можно иметь сколько угодно неизменяемых заимствований, но только одно изменяемое.
-
Rust отслеживает область действия ссылок, не позволяя конфликтов.
-
Это исключает гонки данных, висячие ссылки и двойное освобождение памяти.
-
Поддерживаются механизмы динамического заимствования через RefCell.
Эти строгие, но прозрачные правила делают Rust уникальным среди системных языков, обеспечивая безопасность памяти на этапе компиляции без ущерба для производительности.