$ 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
ГлавнаяБлогПервые страницы Rust: как мыслить через ownership на маленьких примерах

Первые страницы Rust: как мыслить через ownership на маленьких примерах

$ sudo teach IT
·24 июля 2026 г.·10 мин·54
Первые страницы Rust: как мыслить через ownership на маленьких примерах

Соберём набор упражнений, где ownership объясняется на практике: перемещения, заимствования и время жизни ссылок. Цель — перестать бояться компилятора и начать использовать его как подсказчика.

Содержание
Ownership: что компилятор “видит” за васУпражнение 1. Перемещение строк: почему String “уезжает”Минимальный примерЧто происходитВариант “как исправить”Упражнение 2. Copy-типы: когда move не страшенПример с числомЗачем это знатьУпражнение 3. Заимствование неизменяемой ссылкой: &TЗачем заимствования нужныПочему так безопасноУлучшение по стилюУпражнение 4. Заимствование изменяемой ссылкой: &mut T и эксклюзивностьПример: почему нельзя одновременно & и &mutКак исправитьПодводный камень: “ссылка ещё используется”Упражнение 5. Заимствования в функциях: “кто владеет данными?”Вариант 1: функция берёт владение (move)Вариант 2: функция заимствует (borrow)Практический принципУпражнение 6. Время жизни: ссылки не могут “переживать” данныеБазовая ошибкаКак сделать правильно: вернуть владениеКак сделать правильно: возвращать ссылку на данные, которые живут снаружиУпражнение 7. Сравнение времени жизни: два разных 'a и 'bПочему это важноУпражнение 8. Чтение ошибок как подсказок: как “думать вместе с компилятором”Пример типичной правки scopeУпражнение 9. Ownership и коллекции: итераторы, move и “почему вектор пуст”Если нужно сохранить vЕщё один нюанс: если хотите изменить элементыУпражнение 10. “Почему не работает” при передаче владения в несколько местРешение: заимствоватьКогда всё-таки нужен moveКак встроить ownership-мышление в привычки разработки1) Начинайте с “контракта функции”2) Смотрите на области видимости3) Для ссылок выбирайте границы данныхТипичные ошибки на старте (и как их избежать)Ошибка 1: “Я же просто печатаю — почему нельзя?”Ошибка 2: “Я думал, ссылку можно, она же неизменяемая”Ошибка 3: “Время жизни должно быть ‘очевидным’”Ошибка 4: “clone — это всегда решение”Вывод: компилятор как навигатор, а не как противник

Rust часто воспринимают как язык с «магическим компилятором». На самом деле компилятор просто строго проверяет модель владения и времени жизни (ownership и lifetimes). Если вы научитесь читать эти проверки как подсказки, исчезает ключевой страх: «почему я не могу собрать код» превращается в «что именно мне нужно изменить в модели данных».

В этой статье соберём набор маленьких упражнений — не абстрактных лекций, а кусков кода, которые демонстрируют три опоры Rust:

  • перемещение (move) значения и его последствие для исходной переменной;
  • заимствование (borrow) — как безопасно обращаться к данным без передачи владения;
  • время жизни ссылок (lifetimes) — почему компилятор требует гарантий про “кто живёт дольше”.

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


Ownership: что компилятор “видит” за вас

Ownership в Rust — это не про “понятия ради понятий”. Это конкретные правила, которые определяют:

  1. У каждого значения есть владелец — переменная или структура.
  2. В момент перемещения владелец переносится на новое место.
  3. После перемещения старый владелец не имеет права использовать значение.
  4. Ссылки (references) могут быть либо:
    • неизменяемыми &T,
    • изменяемыми &mut T, и при этом должны укладываться в правила времени жизни.

Компилятор не требует от вас “доверия”. Он требует доказательств: “эта ссылка будет валидна здесь и сейчас”.

Дальше — упражнения. Пишите их по порядку: так проще поймать причинно-следственные связи.


Упражнение 1. Перемещение строк: почему String “уезжает”

Минимальный пример

code
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // move

    println!("{}", s2);

    // println!("{}", s1); // ошибка: s1 больше не владеет
}

Что происходит

String — это структура, которая владеет выделенным в куче буфером. Когда вы делаете let s2 = s1;, выполняется move: владение переносится с s1 на s2. Перемещение в Rust не копирует содержимое (как минимум в общем случае), а меняет “владельца”.

Поэтому попытка использовать s1 после move приводит к ошибке компиляции.

Вариант “как исправить”

Если вам нужен исходный String и копия, используйте clone():

code
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1: {}", s1);
    println!("s2: {}", s2);
}

Подводный камень: clone() — это не “дополнительное удобство”. Это явное решение “я действительно хочу ещё одну копию”. Компилятор не угадывает ваши намерения.


Упражнение 2. Copy-типы: когда move не страшен

Пример с числом

code
fn main() {
    let x = 10;
    let y = x; // по сути copy

    println!("x: {}, y: {}", x, y);
}

Числа (и другие типы вроде bool, char, массивы фиксированного размера при определённых условиях) реализуют trait Copy. Для таких типов правило проще: операция присваивания делает копирование. Владение “не уезжает” из переменной — оно просто копируется по значению.

Зачем это знать

Чтобы не путаться: не каждый присваиваемый тип ведёт себя как String. Но принцип один: Rust всегда знает, можно ли копировать без риска.


Упражнение 3. Заимствование неизменяемой ссылкой: &T

Зачем заимствования нужны

Допустим, вы хотите вывести строку, но не менять её и не забирать владение:

code
fn print_len(s: &String) {
    println!("len = {}", s.len());
}

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

    println!("still usable: {}", s);
}

Почему так безопасно

Функция принимает &String, то есть не владение, а заимствование. Владелец остаётся у main и продолжает существовать после вызова.

Компилятор проверяет две вещи:

  1. ссылка &s валидна в момент вызова;
  2. за время жизни ссылки владелец s не был уничтожен и не перешёл в другое владение.

Улучшение по стилю

Rust-разработчики часто избегают конкретизации &String там, где достаточно &str. Это делает функцию более универсальной:

code
fn print_len(s: &str) {
    println!("len = {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(&s); // &String приводится к &str
}

Упражнение 4. Заимствование изменяемой ссылкой: &mut T и эксклюзивность

Неизменяемые ссылки &T допускают много одновременных “наблюдателей”. Изменяемая ссылка &mut T — особая: она даёт право на модификацию и потому должна быть единственной.

Пример: почему нельзя одновременно & и &mut

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

    let r1 = &s;     // неизменяемое заимствование
    let r2 = &mut s; // попытка получить изменяемое заимствование

    r2.push('!');
    println!("{}", r1);
}

Такой код не скомпилируется: одновременно активны r1 и r2, а значит нарушается правило “либо много неизменяемых, либо одна изменяемая”.

Как исправить

Нужно сделать так, чтобы неизменяемая ссылка не жила дольше, чем необходимо. Один из подходов — ограничить область видимости (scope):

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

    {
        let r1 = &s;
        println!("before: {}", r1);
    } // r1 выходит из области видимости — заимствование заканчивается

    let r2 = &mut s;
    r2.push('!');

    println!("after: {}", s);
}

Подводный камень: “ссылка ещё используется”

Иногда кажется, что ссылка “уже не нужна”, но компилятор видит, что она потенциально используется дальше (влияние на время жизни часто проявляется неожиданно). Поэтому ограничения по scope — надёжный инструмент.


Упражнение 5. Заимствования в функциях: “кто владеет данными?”

В учебных примерах полезно явно разделять две роли: владелец и то, что делает функция.

Вариант 1: функция берёт владение (move)

code
fn consume(s: String) {
    println!("consumed: {}", s);
}

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

    // println!("{}", s); // ошибка: s перемещён
}

Вариант 2: функция заимствует (borrow)

code
fn read_only(s: &String) {
    println!("read only: {}", s);
}

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

    println!("still here: {}", s);
}

Практический принцип

  • Хотите, чтобы функция могла “забрать” данные — принимайте T.
  • Хотите, чтобы функция только читала — принимайте &T (или &str).
  • Хотите, чтобы функция могла менять — принимайте &mut T.

Упражнение 6. Время жизни: ссылки не могут “переживать” данные

Базовая ошибка

code
fn make_ref() -> &String {
    let s = String::from("hello");
    &s
}

Этот код не компилируется: s живёт только внутри make_ref. Возвращать ссылку на неё — значит позволить ссылке жить дольше, чем значение, а это недопустимо.

Rust не даёт вам случайно получить висячую ссылку (dangling reference).

Как сделать правильно: вернуть владение

Самый простой ремонт — вернуть String, то есть владение:

code
fn make_string() -> String {
    let s = String::from("hello");
    s
}

fn main() {
    let s = make_string();
    println!("{}", s);
}

Как сделать правильно: возвращать ссылку на данные, которые живут снаружи

Представим функцию, которая выбирает ссылку на одну из двух строк:

code
fn choose_longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world!");

    let res = choose_longer(&s1, &s2);
    println!("{}", res);
}

Здесь <'a> означает: обе входные ссылки должны жить минимум так же долго, как возвращаемая ссылка, и res привязан к общей длительности 'a.


Упражнение 7. Сравнение времени жизни: два разных 'a и 'b

Иногда ссылки живут разное время. Рассмотрим более тонкий случай: возвращаемая ссылка должна быть валидна до истечения более короткой входной ссылки.

code
fn pick_first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str {
    a
}

Параметры 'a и 'b независимы. Возвращается ссылка a, поэтому корректность зависит только от 'a.

Почему это важно

Компилятор управляет не “длительностью в секундах”, а гарантией валидности ссылок. Как только возвращаемая ссылка зависит от одного входа — соответствующая lifetime должна быть связана только с ним.


Упражнение 8. Чтение ошибок как подсказок: как “думать вместе с компилятором”

Ошибки Rust часто выглядят пугающе, но они обычно отвечают на три вопроса:

  1. Кто именно заимствован (конкретная переменная/значение)?
  2. Где оно используется (строка/выражение)?
  3. Что конфликтует (например, изменяемое заимствование при активном неизменяемом)?

Практика: не пытайтесь “угадывать правку”. Сделайте шаги:

  • Найдите конфликт: “cannot borrow … as mutable because … is borrowed as immutable”.
  • Посмотрите на область видимости используемой ссылки. Попробуйте “сжать” scope.
  • Если возвращаемая ссылка указывает на локальную переменную — верните владение или привяжите ссылку к данным более внешнего уровня.

Пример типичной правки scope

code
fn main() {
    let mut v = vec![1, 2, 3];

    let r = &v;         // неизменяемое
    v.push(4);         // попытка изменить вектор
    println!("{:?}", r);
}

Ошибка ожидаемая: пока r жив, менять v нельзя. Решение — изменить порядок и ограничить время жизни r:

code
fn main() {
    let mut v = vec![1, 2, 3];

    {
        let r = &v;
        println!("{:?}", r);
    } // r умерло

    v.push(4);
    println!("{:?}", v);
}

Упражнение 9. Ownership и коллекции: итераторы, move и “почему вектор пуст”

В работе с коллекциями ownership проявляется особенно ярко. Например:

code
fn main() {
    let v = vec![String::from("a"), String::from("b")];

    for s in v {
        println!("{}", s);
    }

    // v больше нельзя использовать: move при итерации
    // println!("{:?}", v);
}

for s in v фактически делает into_iter() и “забирает” элементы — владение переходит в цикл.

Если нужно сохранить v

Используйте заимствующую итерацию:

code
fn main() {
    let v = vec![String::from("a"), String::from("b")];

    for s in &v {
        println!("{}", s);
    }

    println!("v still available: {}", v.len());
}

Ещё один нюанс: если хотите изменить элементы

Нужна изменяемая итерация:

code
fn main() {
    let mut v = vec![String::from("a"), String::from("b")];

    for s in &mut v {
        s.push('!');
    }

    println!("{:?}", v);
}

Упражнение 10. “Почему не работает” при передаче владения в несколько мест

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

code
fn takes_ownership(s: String) {
    println!("{}", s);
}

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

    takes_ownership(s);
    // takes_ownership(s); // ошибка: s уже перемещён
}

Решение: заимствовать

Если функция не должна владеть, меняем сигнатуру:

code
fn takes_borrow(s: &str) {
    println!("{}", s);
}

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

    takes_borrow(&s);
    takes_borrow(&s);
}

Когда всё-таки нужен move

Если функция строит структуру, преобразует данные и должна “забрать” ответственность за ресурс — владение логично. Но это нужно осознанно, а не “потому что компилятор попросил”.


Как встроить ownership-мышление в привычки разработки

К этому моменту вы уже видели основные сценарии: move, borrow, lifetimes. Теперь полезно сформировать устойчивые привычки.

1) Начинайте с “контракта функции”

Перед тем как писать код, ответьте:

  • Функция создаёт результат и может не зависеть от входа? Тогда возвращайте владение.
  • Функция только читает вход? Тогда &T / &str.
  • Функция меняет вход? Тогда &mut T.
  • Функция обрабатывает и “забирает” ресурс? Тогда принимайте T.

Это резко уменьшает число “переезды” владения и конфликты заимствований.

2) Смотрите на области видимости

Когда заимствования конфликтуют, почти всегда помогает ограничить scope.

В Rust lifetime часто “вычисляется” не магией, а обычной видимостью и тем, как компилятор понимает последующее использование переменных.

3) Для ссылок выбирайте границы данных

Если вы видите ошибку про возвращаемые ссылки на локальные значения — не боритесь с компилятором, а поменяйте архитектуру:

  • вернуть владение,
  • или передать данные наружу как аргумент и вернуть ссылку на них.

Типичные ошибки на старте (и как их избежать)

Ошибка 1: “Я же просто печатаю — почему нельзя?”

Если вы передаёте String в функцию и она принимает String, вы не “печатаете”, а передаёте владение. Для печати нужны ссылки.

Ошибка 2: “Я думал, ссылку можно, она же неизменяемая”

Даже неизменяемая ссылка &T может блокировать &mut (или изменение самого владения) до конца её области жизни.

Ошибка 3: “Время жизни должно быть ‘очевидным’”

Компилятор не верит человеческой очевидности. Если ссылка возвращается наружу — Rust требует доказательств через lifetimes (в явном виде или через вывод).

Ошибка 4: “clone — это всегда решение”

clone() полезен, но он меняет модель производительности и семантику. На старте лучше научиться отделять “заимствование” от “копирования”.


Вывод: компилятор как навигатор, а не как противник

Ownership в Rust поначалу кажется жёстким, потому что язык не позволяет “интуитивно неправильные” вещи вроде использования после move или возвращения ссылок на уже уничтоженные данные. Но эта жёсткость — плата за безопасность: Rust не даёт получить класс ошибок, которые в системных языках часто стоят времени отладки.

Рабочая стратегия — делать маленькие упражнения и читать ошибки как инструкции:

  • move произошло? значит, владение ушло — используйте borrow или clone по необходимости;
  • заимствования конфликтуют? значит, вы нарушили правило эксклюзивности — попробуйте ограничить scope или поменять типы ссылок;
  • ссылка невалидна “снаружи”? значит, нужна другая архитектура — вернуть владение или корректно связать lifetimes.

Если вы хотите дальше углубляться и собрать эти правила в более системный набор практик, можно попробовать учебный курс Rust — как способ структурировать упражнения, а не как попытку “обойти

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

Автор

$ sudo teach IT

Rust

Курс по теме

Rust

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

Открыть курс

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

16 июля 2026 г.
690
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 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

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

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

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

Комментарии

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

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

Содержание

Ownership: что компилятор “видит” за васУпражнение 1. Перемещение строк: почему String “уезжает”Минимальный примерЧто происходитВариант “как исправить”Упражнение 2. Copy-типы: когда move не страшенПример с числомЗачем это знатьУпражнение 3. Заимствование неизменяемой ссылкой: &TЗачем заимствования нужныПочему так безопасноУлучшение по стилюУпражнение 4. Заимствование изменяемой ссылкой: &mut T и эксклюзивностьПример: почему нельзя одновременно & и &mutКак исправитьПодводный камень: “ссылка ещё используется”Упражнение 5. Заимствования в функциях: “кто владеет данными?”Вариант 1: функция берёт владение (move)Вариант 2: функция заимствует (borrow)Практический принципУпражнение 6. Время жизни: ссылки не могут “переживать” данныеБазовая ошибкаКак сделать правильно: вернуть владениеКак сделать правильно: возвращать ссылку на данные, которые живут снаружиУпражнение 7. Сравнение времени жизни: два разных 'a и 'bПочему это важноУпражнение 8. Чтение ошибок как подсказок: как “думать вместе с компилятором”Пример типичной правки scopeУпражнение 9. Ownership и коллекции: итераторы, move и “почему вектор пуст”Если нужно сохранить vЕщё один нюанс: если хотите изменить элементыУпражнение 10. “Почему не работает” при передаче владения в несколько местРешение: заимствоватьКогда всё-таки нужен moveКак встроить ownership-мышление в привычки разработки1) Начинайте с “контракта функции”2) Смотрите на области видимости3) Для ссылок выбирайте границы данныхТипичные ошибки на старте (и как их избежать)Ошибка 1: “Я же просто печатаю — почему нельзя?”Ошибка 2: “Я думал, ссылку можно, она же неизменяемая”Ошибка 3: “Время жизни должно быть ‘очевидным’”Ошибка 4: “clone — это всегда решение”Вывод: компилятор как навигатор, а не как противник