$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогOwnership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)

Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)

$ sudo teach IT
·16 июля 2026 г.·9 мин·69
Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)

Пошагово разбираем систему владения Rust, объясняем, почему она исключает целые классы ошибок памяти и как научиться думать в её терминах с первых дней.

Содержание
Почему управление памятью вообще сложноOwnership: одна переменная — один хозяинПеремещение (Move)Копирование (Copy)Ownership и функцииBorrowing: дать попользоваться, не отдаваяПравила заимствованияNLL: Non-Lexical LifetimesLifetimes: когда компилятор просит помощиТипичные ошибки и как их читатьОшибка: использование после перемещенияОшибка: изменяемая ссылка при существующих неизменяемыхКак научиться думать в терминах ownershipУмный указатель и interior mutability: когда правила нужно обойтиЧто это даёт на практикеВывод

Rust регулярно занимает первое место в опросах Stack Overflow как «самый любимый язык» — и это при том, что порог вхождения у него заметно выше, чем у большинства современных языков. Причина такой любви парадоксальна: разработчики ценят Rust именно за то, что он не даёт им делать то, что они привыкли делать в C, C++ или даже Python.

Ключевая идея — система ownership и borrowing. Это не просто синтаксическая особенность и не набор ограничений ради ограничений. Это фундаментально иной способ думать об управлении памятью, который позволяет компилятору гарантировать безопасность там, где другие языки полагаются на дисциплину программиста или сборщик мусора. Разберём, как это работает — честно, с примерами и типичными ошибками.


Почему управление памятью вообще сложно

Прежде чем говорить о Rust, стоит понять, какую именно проблему он решает.

В языках без сборки мусора (C, C++) программист вручную выделяет и освобождает память. Это даёт контроль и производительность, но открывает целый класс ошибок:

  • Use-after-free — обращение к памяти после её освобождения
  • Double-free — попытка освободить одну и ту же память дважды
  • Dangling pointers — указатели на уже несуществующие данные
  • Data races — одновременное изменение данных из нескольких потоков

По данным Microsoft и Google, около 70% критических уязвимостей в их кодовых базах на C/C++ связаны именно с ошибками управления памятью. Это не проблема конкретных программистов — это системная проблема инструмента.

Языки со сборщиком мусора (Java, Python, Go) решают большую часть этих проблем автоматически, но платят за это: GC-паузы, дополнительный расход памяти и невозможность точно предсказать, когда именно освободится ресурс.

Rust выбрал третий путь: статический анализ времени компиляции. Компилятор проверяет корректность работы с памятью до запуска программы — и если что-то не так, просто не даёт скомпилировать код.


Ownership: одна переменная — один хозяин

Центральное правило Rust звучит просто: у каждого значения есть ровно один владелец. Когда владелец выходит из области видимости, значение уничтожается.

code
fn main() {
    let s = String::from("hello"); // s — владелец строки
    println!("{}", s);
} // здесь s выходит из области видимости, память освобождается автоматически

Никакого free(), никакого GC. Компилятор знает, где заканчивается область видимости, и вставляет вызов деструктора автоматически.

Перемещение (Move)

Когда вы присваиваете одну переменную другой, происходит не копирование — происходит перемещение (move). Прежний владелец перестаёт существовать.

code
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // владение перешло к s2

    println!("{}", s1); // ОШИБКА: s1 больше не валиден
}

Компилятор выдаст ошибку:

code
error[E0382]: borrow of moved value: `s1`

На первый взгляд это кажется странным ограничением. На самом деле это защита от double-free: если бы оба s1 и s2 владели одной строкой, при выходе из области видимости память освободилась бы дважды.

Копирование (Copy)

Для простых типов, которые целиком живут на стеке (числа, булевы значения, символы), Rust использует копирование — здесь нет проблемы двойного освобождения, потому что нет кучи.

code
fn main() {
    let x = 5;
    let y = x; // x копируется, оба валидны

    println!("x = {}, y = {}", x, y); // работает
}

Тип реализует трейт Copy — это явный сигнал компилятору и читателю кода, что копирование дёшево и безопасно.

Ownership и функции

Передача значения в функцию работает по тем же правилам: функция становится новым владельцем.

code
fn takes_ownership(s: String) {
    println!("{}", s);
} // s уничтожается здесь

fn main() {
    let s = String::from("hello");
    takes_ownership(s);

    println!("{}", s); // ОШИБКА: s уже перемещён
}

Это означает, что если вы хотите использовать значение после вызова функции, нужно либо вернуть его обратно, либо — что намного удобнее — использовать заимствование.


Borrowing: дать попользоваться, не отдавая

Borrowing — это механизм временного доступа к значению без передачи владения. В терминах Rust: вы создаёте ссылку на значение.

code
fn calculate_length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // передаём ссылку, не владение

    println!("Длина '{}' равна {}", s, len); // s по-прежнему валиден
}

&s — это ссылка на s. Функция calculate_length «заимствует» строку: может её читать, но не владеет ею. Когда функция завершается, ссылка исчезает, но сама строка остаётся нетронутой.

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

Система borrowing строится на двух правилах, которые компилятор проверяет статически:

  1. В любой момент времени может существовать либо любое количество неизменяемых ссылок (&T), либо ровно одна изменяемая ссылка (&mut T).
  2. Ссылки всегда должны быть валидными.

Это не произвольные ограничения — это формализация того, что опытные программисты и так пытаются соблюдать вручную. Первое правило исключает data races: если кто-то читает данные, никто не может их одновременно изменять. Второе правило исключает dangling pointers.

code
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;     // неизменяемая ссылка — ок
    let r2 = &s;     // ещё одна неизменяемая — ок
    let r3 = &mut s; // ОШИБКА: уже есть неизменяемые ссылки

    println!("{}, {}, {}", r1, r2, r3);
}
code
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

NLL: Non-Lexical Lifetimes

Начиная с Rust 2018, компилятор умеет определять, что ссылка перестала использоваться раньше конца своей лексической области видимости. Это делает код менее строгим в хорошем смысле:

code
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} и {}", r1, r2);
    // r1 и r2 больше не используются после этой строки

    let r3 = &mut s; // теперь это валидно
    println!("{}", r3);
}

Компилятор видит, что r1 и r2 «умерли» после println!, поэтому создание изменяемой ссылки r3 уже не конфликтует с ними.


Lifetimes: когда компилятор просит помощи

Иногда компилятор не может самостоятельно определить, насколько долго должна жить ссылка. В таких случаях нужно явно указать время жизни (lifetime).

code
// Без аннотации компилятор не знает, чья жизнь должна быть длиннее
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

'a — это параметр времени жизни. Он говорит: «возвращаемая ссылка живёт не дольше, чем самый короткий из входных аргументов». Компилятор использует эту информацию, чтобы убедиться: никто не будет держать ссылку на уже уничтоженные данные.

Lifetimes — одна из самых сложных частей Rust для новичков. Хорошая новость: в большинстве случаев компилятор выводит их автоматически благодаря правилам lifetime elision, и явно указывать их приходится значительно реже, чем кажется поначалу.


Типичные ошибки и как их читать

Одно из реальных достоинств Rust — качество сообщений об ошибках. Компилятор не просто говорит «что-то не так», он объясняет что именно и часто предлагает решение.

Ошибка: использование после перемещения

code
fn main() {
    let v = vec![1, 2, 3];
    let v2 = v;
    println!("{:?}", v); // error[E0382]
}

Решение: если нужна копия — используйте .clone(). Если нужен только доступ — используйте ссылку &v.

Ошибка: изменяемая ссылка при существующих неизменяемых

code
fn main() {
    let mut data = vec![1, 2, 3];
    let first = &data[0];
    data.push(4); // error[E0502]
    println!("{}", first);
}

Проблема здесь неочевидна: push может перераспределить память вектора, что сделает first dangling pointer. Компилятор это видит — вы, возможно, нет.

Решение: получить first после push, или скопировать значение: let first = data[0];.


Как научиться думать в терминах ownership

Самая распространённая ошибка новичков в Rust — пытаться «обмануть» компилятор или переписывать код до тех пор, пока он не скомпилируется, не понимая почему. Это путь в никуда.

Правильный подход — задавать себе вопросы при написании каждой функции:

  • Кому нужно владение? Если функция должна хранить значение или уничтожить его — ей нужно владение.
  • Кому нужен только доступ на чтение? Используйте &T.
  • Кому нужен доступ на изменение? Используйте &mut T.
  • Нужно ли значение после передачи? Если да — передавайте ссылку, а не само значение.

Эта модель мышления поначалу требует усилий, но быстро становится интуитивной. Многие опытные Rust-разработчики отмечают, что после нескольких месяцев работы с языком начинают замечать потенциальные проблемы с памятью в коде на других языках — именно потому, что Rust научил их думать явно о владении данными.

Для тех, кто хочет пройти этот путь структурированно, с практическими задачами и разбором реальных сценариев, есть смысл посмотреть на курс по Rust — там система типов и модель владения разбираются последовательно, от основ до продвинутых паттернов.


Умный указатель и interior mutability: когда правила нужно обойти

Иногда строгие правила borrowing мешают реализовать вполне легитимные паттерны. Для таких случаев Rust предоставляет escape hatches — механизмы, которые переносят проверки с времени компиляции на время выполнения.

RefCell<T> позволяет изменять данные через неизменяемую ссылку, проверяя правила borrowing в рантайме:

code
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    let r1 = data.borrow();     // неизменяемое заимствование
    println!("{:?}", *r1);
    drop(r1);                   // явно освобождаем

    data.borrow_mut().push(4);  // теперь можно изменить
    println!("{:?}", data.borrow());
}

Rc<T> и Arc<T> позволяют иметь несколько владельцев (подсчёт ссылок), а Arc делает это потокобезопасно.

Важно понимать: это не «обход» системы безопасности. RefCell паникует в рантайме при нарушении правил, а не молча допускает UB. Это осознанный компромисс между строгостью компиляции и гибкостью кода.


Что это даёт на практике

Система ownership — не академическая концепция. Вот что она означает в реальных проектах:

Нет GC-пауз. Память освобождается детерминированно, в момент выхода из области видимости. Это критично для систем реального времени, игровых движков, встроенных систем.

Безопасность без overhead. Проверки выполняются на этапе компиляции. В рантайме нет никакого дополнительного кода для отслеживания владения — только деструкторы там, где они нужны.

Потокобезопасность по умолчанию. Правило «одна изменяемая ссылка» автоматически исключает data races. Если код скомпилировался — он не имеет гонок данных. Это гарантия, которую не даёт ни один другой системный язык.

Лучшая документация через типы. Сигнатура функции fn process(data: &mut Vec<i32>) явно говорит: «я изменю этот вектор». fn read(data: &Vec<i32>) — «я только читаю». Это не комментарий, который может устареть, — это контракт, который компилятор проверяет.


Вывод

Система ownership и borrowing — это не особенность Rust, которую нужно «пережить». Это его главная ценность. Компилятор действительно умнее среднестатистического программиста в одном конкретном смысле: он никогда не устаёт, никогда не торопится и никогда не забывает проверить все пути исполнения кода.

Первые недели с Rust могут быть frustrating: код не компилируется, ошибки кажутся придирками, хочется вернуться к привычным инструментам. Но за этим порогом — язык, который позволяет писать системный код с уверенностью, которой просто не существует в C или C++.

Лучший способ пройти этот порог — не читать документацию пассивно, а писать код и разбирать ошибки компилятора. Если хочется делать это в структурированной среде с обратной связью, курс по Rust может стать хорошей отправной точкой. Но даже без него — читайте «The Rust Book», пишите код, доверяйте компилятору. Он на вашей стороне.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Rust

Курс по теме

Rust

Rust — с нуля до уверенной работы с самым безопасным системным языком. Для начинающих и опытных. Освойте владение, трейты, многопоточность и async — и пишите надёжный код без страха утечек памяти.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

GIL в CPython: почему многопоточность не всегда ускоряет ваш Python-код
cpython

GIL в CPython: почему многопоточность не всегда ускоряет ваш Python-код

Разбираем, как устроен Global Interpreter Lock, почему потоки в Python не работают параллельно на CPU-задачах и когда стоит переключиться на multiprocessing или asyncio.

16 июля 2026 г.
710
Memory management в Rust: стек, куча и когда данные действительно удаляются
memory

Memory management в Rust: стек, куча и когда данные действительно удаляются

Объясняем, как Rust управляет памятью без сборщика мусора — через владение, время жизни и Drop. Сравниваем с поведением C++ и показываем, почему подход Rust исключает целый класс ошибок.

18 июля 2026 г.
660
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему управление памятью вообще сложноOwnership: одна переменная — один хозяинПеремещение (Move)Копирование (Copy)Ownership и функцииBorrowing: дать попользоваться, не отдаваяПравила заимствованияNLL: Non-Lexical LifetimesLifetimes: когда компилятор просит помощиТипичные ошибки и как их читатьОшибка: использование после перемещенияОшибка: изменяемая ссылка при существующих неизменяемыхКак научиться думать в терминах ownershipУмный указатель и interior mutability: когда правила нужно обойтиЧто это даёт на практикеВывод