Перейти к содержанию
Educora
Средний22 мин4 / 10

Владение, заимствование и срезы

Изучи главную идею Rust: у каждого значения один владелец. Разберись с перемещением (move), ссылками `&` и `&mut`, правилами заимствования, срезами и разницей между `String` и `&str`.

Проверь себя
В этом уроке ты узнаешь
  • Формулировать три правила владения и объяснять разницу между перемещением и копированием
  • Передавать значения в функции через ссылки & и &mut, не забирая владение
  • Читать и исправлять ошибки E0382, E0499 и E0502
  • Работать со срезами и правильно выбирать между String и &str

У тебя есть книга. Ты можешь подарить её другу — тогда она уже не твоя. Можешь дать её почитать сразу нескольким друзьям. А можешь дать только одному другу делать в ней пометки — пока он пишет, никто другой не должен её читать, чтобы не увидеть незаконченные записи. Rust управляет памятью именно по этой логике: владение, заимствование для чтения и заимствование для изменения.

Стек, куча и правила владения

Значения, размер которых известен заранее (i32, bool, char), хранятся в быстром стеке. Данные переменного размера, например растущая строка String, лежат в динамической памяти — куче: в стеке остаются только указатель, длина и ёмкость. Кто-то должен освободить память в куче — в Rust это работа владельца.

  1. У каждого значения в Rust есть владелец — переменная, которая его хранит.
  2. В каждый момент владелец может быть только один.
  3. Когда владелец выходит из области видимости (кончается его блок { }), значение автоматически удаляется (drop), а память освобождается.

Перемещение, клонирование и Copy

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

Ошибка E0382
fn main() {
    let s1 = String::from("Baku");
    let s2 = s1;
    println!("{s1} {s2}");
}
Правильно с `clone`
fn main() {
    let s1 = String::from("Baku");
    let s2 = s1.clone();
    println!("{s1} {s2}");
}
Программа справа выводит Baku Baku. clone создаёт полную копию данных в куче, поэтому для больших данных это дорого.
Text
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:15
  |
2 |     let s1 = String::from("Baku");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1} {s2}");
  |               ^^^^ value borrowed here after move

Целые и дробные числа, bool, char и кортежи только из них — типы Copy: они целиком живут в стеке, и копировать их дёшево. Такие значения не перемещаются, а копируются — после let b = a; обе переменные рабочие. Передача аргумента в функцию подчиняется тому же правилу:

Rust
fn take(text: String) {
    println!("took: {text}");
}

fn show(n: i32) {
    println!("copied: {n}");
}

fn main() {
    let city = String::from("Ganja");
    take(city);
    // println!("{city}"); // error E0382: city was moved

    let year = 2024;
    show(year);
    println!("still have: {year}");
}
Ожидаемый результат
took: Ganja
copied: 2024
still have: 2024
take стала владельцем String и удалила её в конце. А year имеет тип i32, поэтому была передана лишь копия.

Ссылки и заимствование

Каждый раз отдавать владение и забирать его обратно утомительно. Вместо этого значение можно одолжить: &name создаёт ссылку, которая не забирает владение. Функция читает значение через ссылку, а владелец после этого не меняется. Чтобы изменить значение, нужна изменяемая ссылка &mut — и сама переменная тоже должна быть mut.

Rust
fn length(text: &String) -> usize {
    text.len()
}

fn add_suffix(text: &mut String) {
    text.push_str(" city");
}

fn main() {
    let mut name = String::from("Baku");
    let len = length(&name);
    println!("{name} has {len} letters");

    add_suffix(&mut name);
    println!("{name}");
}
Ожидаемый результат
Baku has 4 letters
Baku city
Определение
Правила заимствования

В любой момент у значения может быть либо сколько угодно ссылок &, либо ровно одна ссылка &mut — но не то и другое вместе. Кроме того, ссылки всегда должны быть действительными: ссылка не может жить дольше владельца. Ссылка «заканчивается» там, где её использовали в последний раз.

Ошибка E0499
fn main() {
    let mut s = String::from("hi");
    let r1 = &mut s;
    let r2 = &mut s;
    r1.push('!');
    r2.push('?');
    println!("{s}");
}
Правильно: выводит `hi!?`
fn main() {
    let mut s = String::from("hi");
    let r1 = &mut s;
    r1.push('!');
    let r2 = &mut s;
    r2.push('?');
    println!("{s}");
}
Слева две ссылки &mut «живы» одновременно (cannot borrow s as mutable more than once at a time). Справа r1 в последний раз используется в push и заканчивается до создания r2.
Ошибка E0502
fn main() {
    let mut scores = vec![90, 85];
    let first = &scores[0];
    scores.push(70);
    println!("{first}");
}
Правильно: выводит `90`
fn main() {
    let mut scores = vec![90, 85];
    let first = scores[0];
    scores.push(70);
    println!("{first}");
}
vec![...] создаёт расширяемый список (Vec). Справа first — не ссылка, а копия значения i32, поэтому список можно менять.
Text
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &scores[0];
  |                  ------ immutable borrow occurs here
4 |     scores.push(70);
  |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |               ------- immutable borrow later used here

Срезы: String и &str

Срез (slice) — ссылка на часть коллекции: &numbers[1..4] показывает элементы с индексами 1, 2 и 3. Текстовый срез имеет тип &str. Любой строковый литерал в программе ("hello") — тоже &str: ссылка на текст, хранящийся в самом файле программы. А String владеет своими данными, живёт в куче и может расти.

Rust
fn first_word(text: &str) -> &str {
    for (i, ch) in text.char_indices() {
        if ch == ' ' {
            return &text[..i];
        }
    }
    text
}

fn main() {
    let sentence = String::from("hello brave world");
    let word = first_word(&sentence);
    println!("{word}");
    println!("{}", first_word("Rust"));

    let numbers = [10, 20, 30, 40, 50];
    let middle = &numbers[1..4];
    println!("{:?} len={}", middle, middle.len());
}
Ожидаемый результат
hello
Rust
[20, 30, 40] len=3
first_word принимает и &String, и литерал: Rust автоматически превращает &String в &str. &text[..i] — часть от начала до байта i.
СвойствоString&str
Владениевладеет даннымизаимствует
Изменениес mut может расти: push_strтолько чтение
СозданиеString::from("hi"), "hi".to_string()"hi", &s, &s[0..2]
Типичное местополя структур, сборка текстапараметры функций

Главное

  • У каждого значения один владелец; когда он выходит из области видимости, значение удаляется.
  • Типы вроде String при присваивании и передаче в функцию перемещаются; типы Copy копируются.
  • & заимствует для чтения, &mut — для изменения; владение при этом не меняется.
  • Одновременно может быть много & или одна &mut; ссылка не живёт дольше владельца.
  • &str — текстовый срез, String — владеющий растущий текст; в параметрах пиши &str.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какую переменную можно использовать после let a = String::from("x"); let b = a;?