временный доступ без передачи владенияtemporary access without transferring ownership
&T — разделяемая ссылка, их может быть сколько угодно одновременно. &mut T — эксклюзивная ссылка, в любой момент существует не более одной, и она не может сосуществовать ни с одной &T на те же данные. Это правило "aliasing xor mutability" компилятор проверяет полностью на этапе компиляции, без единого байта рантайм-оверхеда.&T is a shared reference — any number can exist at once. &mut T is an exclusive reference — at most one can exist at a time, and it cannot coexist with any &T to the same data. This "aliasing xor mutability" rule is checked entirely at compile time by the compiler, with zero bytes of runtime overhead.
Borrowing — способ получить временный доступ к значению через ссылку, не забирая владение (см. ownership.html — читать сначала, это обязательный предварительный материал). &T — разделяемая (неизменяемая) ссылка: таких ссылок на одни данные может существовать сколько угодно одновременно, и через них нельзя мутировать данные. &mut T — эксклюзивная (изменяемая) ссылка: в любой момент времени существует максимум одна, и пока она жива, никакая другая ссылка (ни &T, ни другая &mut T) на те же данные существовать не может. Это и есть правило "aliasing xor mutability" (алиасинг исключает мутируемость): либо сколько угодно читателей, либо ровно один писатель — никогда одновременно оба.Borrowing is a way to get temporary access to a value through a reference without taking ownership (see ownership.html — read it first, it's a mandatory prerequisite). &T is a shared (immutable) reference: any number of them can exist simultaneously for the same data, and none of them can mutate it. &mut T is an exclusive (mutable) reference: at most one can exist at any given time, and while it's alive no other reference (neither &T nor another &mut T) to the same data may exist. This is the "aliasing xor mutability" rule: either any number of readers, or exactly one writer — never both at once.
fn print_len(v: &Vec<i32>) {
println!("len = {}", v.len()); // только чтение через разделяемую ссылку// read-only access through a shared reference
}
fn main() {
let data = vec![1, 2, 3];
print_len(&data); // заимствование — data остаётся владельцем// borrowed — data remains the owner
println!("{:?}", data); // OK: data всё ещё доступна после вызова// OK: data is still accessible after the call
}
Ownership без borrowing был бы практически неюзабельным — вот что решает этот механизм:Ownership without borrowing would be practically unusable — here's what this mechanism solves:
Без ссылок каждый вызов функции, которой нужно просто посмотреть на значение (напечатать его, прочитать поле), забирал бы владение (move-семантика) — вызывающий код терял бы доступ к значению после вызова. Это абсолютно непрактично для базовых операций вроде "напечатать переменную" или "прочитать длину вектора". Borrowing даёт временный, проверяемый доступ без накладных расходов и неудобств постоянной передачи владения туда-обратно.Without references, every function call that merely needs to inspect a value (print it, read a field) would take ownership via move semantics — the caller would lose access to the value afterward. That's completely impractical for basics like "print a variable" or "read a vector's length". Borrowing gives temporary, checked access without the overhead and awkwardness of constantly transferring ownership back and forth.
Правило "либо много читателей, либо один писатель" — не просто соглашение, а механически проверяемая гарантия. Это ровно то условие, которое предотвращает гонки данных на этапе компиляции: гонка данных требует одновременного доступа на запись и чтение/запись из разных мест — а borrow checker физически не даёт скомпилировать код, где такое одновременное существование ссылок возможно.The "either many readers or one writer" rule isn't just a convention — it's a mechanically checked guarantee. It's exactly the condition that prevents data races at compile time: a data race requires simultaneous write access alongside read/write access from elsewhere — and the borrow checker physically refuses to compile code where such simultaneous references could exist.
В C++ "не изменяй контейнер во время итерации по нему" — общеизвестное правило, но ничто не мешает нарушить его синтаксически: код скомпилируется и упадёт (или тихо испортит данные) только в рантайме, при инвалидации итераторов. В более широком смысле, алиасинг указателей в C/C++ — известный источник undefined behavior и ловушек для оптимизатора компилятора. Rust превращает это правило из конвенции в механически проверяемый инвариант языка.In C++, "never modify a container while iterating over it" is a well-known rule, but nothing stops you from violating it syntactically: the code compiles and only fails (or silently corrupts data) at runtime, via iterator invalidation. More broadly, pointer aliasing in C/C++ is a well-known source of undefined behavior and compiler-optimization footguns. Rust turns this rule from a convention into a mechanically enforced language invariant.
Попытка создать &mut T, пока жива &T на те же данные, — ошибка компиляции:Attempting to create a &mut T while a &T to the same data is alive is a compile error:
let mut v = vec![1, 2, 3];
let r1 = &v; // разделяемая ссылка создана// shared reference created
let r2 = &mut v; // ОШИБКА: нельзя создать &mut, пока жива &// ERROR: cannot create &mut while a & is alive
println!("{:?}", r1);
println!("{:?}", r2);
Последовательные, не перекрывающиеся во времени заимствования компилируются без проблем — заимствования не обязаны никогда не пересекаться в коде, важно лишь, чтобы их фактические времена жизни не перекрывались:Sequential, non-overlapping borrows compile without any issue — borrows don't need to avoid overlapping in the source, only their actual live ranges must not overlap:
let mut v = vec![1, 2, 3];
{
let r1 = &v;
println!("{:?}", r1); // r1 используется в последний раз здесь// r1's last use is here
}
let r2 = &mut v; // OK: r1 уже не жив// OK: r1 is no longer alive
r2.push(4);
С Rust 2018 borrow checker использует так называемые "non-lexical lifetimes" (NLL) — он отслеживает фактическое последнее использование заимствования, а не его лексическую область видимости до конца блока. Это значит, что &T, которая больше не используется дальше по коду, не блокирует последующую &mut T, даже если формально обе "видны" до конца блока — фигурные скобки из примера выше на самом деле не нужны:Since Rust 2018, the borrow checker uses so-called "non-lexical lifetimes" (NLL) — it tracks a borrow's actual last use, not its lexical scope to the end of the block. This means a &T that's no longer used later in the code doesn't block a subsequent &mut T, even though both are lexically "in scope" until the end of the block — the braces in the example above aren't actually required:
let mut v = vec![1, 2, 3];
let r1 = &v;
println!("{:?}", r1); // последнее использование r1 — здесь// r1's last use is here
let r2 = &mut v; // OK благодаря NLL, хотя r1 лексически ещё "в области видимости"// OK thanks to NLL, even though r1 is lexically still "in scope"
r2.push(4);
Функция принимает &Vec<T>, а не Vec<T> — вызывающий код сохраняет доступ к вектору после вызова. Если бы функция принимала Vec<T> по значению, вектор был бы перемещён внутрь функции, и вызывающий код терял бы к нему доступ навсегда (если только функция не вернула бы его обратно).The function takes &Vec<T>, not Vec<T> — the caller retains access to the vector after the call. If the function took Vec<T> by value, the vector would be moved into the function, and the caller would lose access to it permanently (unless the function returned it back).
fn print_all(items: &Vec<String>) {
for item in items {
println!("{}", item);
}
}
let names = vec![String::from("a"), String::from("b")];
print_all(&names); // заимствование// borrowed
println!("{}", names.len()); // OK: names всё ещё владелец// OK: names is still the owner
// если бы print_all(items: Vec<String>) — names было бы перемещено и недоступно ниже// if print_all(items: Vec<String>) — names would be moved and unavailable below
Цикл держит &Vec, пока push требует &mut Vec — компилятор отвергает такой код. В C++ эквивалентный код (диапазонный for с push_back внутри) компилируется без единого предупреждения и инвалидирует итераторы в рантайме — классический источник UB, который в Rust просто невозможно написать.The loop holds a &Vec while push requires a &mut Vec — the compiler rejects this code outright. In C++ the equivalent code (a range-for with push_back inside) compiles without a single warning and invalidates iterators at runtime — a classic UB source that simply cannot be written in Rust.
let mut v = vec![1, 2, 3];
for x in &v {
v.push(*x); // ОШИБКА: v занята через &v в цикле, push требует &mut v// ERROR: v is borrowed as &v by the loop, push needs &mut v
}
// C++: for (int x : v) { v.push_back(x); } — компилируется, UB в рантайме// C++: for (int x : v) { v.push_back(x); } — compiles, UB at runtime
Borrow checker видит, что фактическое последнее использование &mut-заимствования заканчивается до начала нового заимствования, и разрешает код, хотя обе ссылки формально "в области видимости" одного блока.The borrow checker sees that the &mut borrow's actual last use ends before the new borrow begins, and allows the code, even though both references are formally "in scope" within the same block.
let mut s = String::from("hi");
let m = &mut s;
m.push_str("!"); // последнее использование m — здесь// m's last use is here
let r = &s; // OK: NLL видит, что m больше не понадобится// OK: NLL sees that m is no longer needed
println!("{}", r);
Метод, который читает состояние, принимает &self; метод, который его меняет, — &mut self; метод, который потребляет объект целиком (например, преобразует его в другой тип), — self по значению. Эти три формы напрямую отражают три режима владения/заимствования из этой темы.A method that reads state takes &self; one that mutates it takes &mut self; one that consumes the object entirely (e.g. converts it into another type) takes self by value. These three forms map directly onto the three ownership/borrowing modes covered in this topic.
struct Counter { value: i32 }
impl Counter {
fn get(&self) -> i32 { self.value } // только чтение// read-only
fn increment(&mut self) { self.value += 1; } // изменение на месте// mutates in place
fn into_value(self) -> i32 { self.value } // потребляет self целиком// consumes self entirely
}
"Изменяемая" в &mut T относится к возможности мутировать данные, НА КОТОРЫЕ указывает ссылка, а не к самой ссылке как переменной. Через &mut T нельзя заставить её указывать на другой объект — только менять содержимое того, на что она уже указывает (через разыменование *r = ...). Это частая путаница у новичков, ожидающих, что "mutable reference" значит "переменную-ссылку можно переприсвоить"."Mutable" in &mut T refers to the ability to mutate the data the reference points to, not the reference variable itself. A &mut T cannot be made to point at a different object — it can only change the contents of what it already points to (via dereferencing, *r = ...). This is a common point of confusion for newcomers expecting "mutable reference" to mean "the reference variable can be reassigned".
Правила borrow checker'а намеренно консервативны: код, безопасность которого программист может доказать собственными рассуждениями, может быть отвергнут, если анализатор не в состоянии это формально доказать — особенно на самоссылочных структурах или при заимствовании разных полей одной структуры одновременно. Это реальный и распространённый источник фрустрации у новичков ("борьба с borrow checker'ом").The borrow checker's rules are deliberately conservative: code whose safety the programmer can reason about themselves can still be rejected if the checker's analysis can't formally prove it — particularly with self-referential structures or patterns that split borrows across struct fields. This is a real and common source of frustration for newcomers ("fighting the borrow checker").
Возвращаемая из функции ссылка должна быть привязана ко времени жизни чего-то, чем всё ещё владеет вызывающий код, — вернуть &T на значение, созданное внутри самой функции, невозможно: оно будет уничтожено при выходе из функции, и ссылка станет висячей. Сообщение компилятора об ошибке в этом случае прямо говорит "borrowed value does not live long enough" — это прямой анонс следующей темы, lifetimes.html.A reference returned from a function must be tied to the lifetime of something the caller still owns — you cannot return a &T to a value created inside the function itself: it gets dropped when the function exits, leaving the reference dangling. The compiler's error message in this case explicitly says "borrowed value does not live long enough" — a direct preview of the next topic, lifetimes.html.