временный доступ без передачи владенияtemporary access without transferring ownership

Borrowing и ссылкиBorrowing and references

&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.

основа языкаcore of the language частый вопрос: почему нельзя одновременно &T и &mut Tcommon question: why can't &T and &mut T coexist

Что этоWhat it is

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
}

Зачем ввелиWhy it was introduced

Ownership без borrowing был бы практически неюзабельным — вот что решает этот механизм:Ownership without borrowing would be practically unusable — here's what this mechanism solves:

01

Ownership без borrowing непрактиченOwnership without borrowing is impractical

Без ссылок каждый вызов функции, которой нужно просто посмотреть на значение (напечатать его, прочитать поле), забирал бы владение (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.

02

Aliasing xor mutability предотвращает гонки данных механическиAliasing xor mutability prevents data races mechanically

Правило "либо много читателей, либо один писатель" — не просто соглашение, а механически проверяемая гарантия. Это ровно то условие, которое предотвращает гонки данных на этапе компиляции: гонка данных требует одновременного доступа на запись и чтение/запись из разных мест — а 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.

03

В C++ то же правило — только соглашение, а не проверкаIn C++ the same rule is only a convention, never checked

В 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.

Как это работает под капотомHow it works under the hood

Попытка создать &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);

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Чтение содержимого без забора владенияReading contents without taking ownership

Функция принимает &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
02

Предотвращение инвалидации итератора на этапе компиляцииPreventing iterator invalidation at compile time

Цикл держит &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
03

NLL в действии: &mut, используемая один раз, не мешает последующей &NLL in action: a &mut used once doesn't block a later &

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);
04

Три формы receiver — &self, &mut self, selfThe three receiver forms — &self, &mut self, self

Метод, который читает состояние, принимает &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
}

ГраблиGotchas

&mut T не позволяет "переставить" саму ссылку через присваивание переменной&mut T doesn't let you reseat the reference itself

"Изменяемая" в &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 консервативен — иногда отвергает заведомо безопасный кодThe borrow checker is conservative — it sometimes rejects code that's actually safe

Правила 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").

Нельзя вернуть ссылку на значение, созданное внутри функцииYou can't return a reference to a value created inside the function

Возвращаемая из функции ссылка должна быть привязана ко времени жизни чего-то, чем всё ещё владеет вызывающий код, — вернуть &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.