Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Массивы в Rust

Что вы узнаете: основные структуры данных Rust — массивы, кортежи, срезы, строки, структуры, Vec и HashMap. Глава плотная; сосредоточьтесь на понимании String и &str и на том, как работают структуры. Ссылки и заимствования вы разберёте подробно в главе 7.

  • Массивы содержат фиксированное количество элементов одного типа
    • Как и все остальные типы Rust, массивы по умолчанию неизменяемые (если не используется mut)
    • Доступ к элементам массива осуществляется через [] и проверяется на выход за границы. Длину массива можно получить методом len()
    fn get_index(y : usize) -> usize {
        y+1        
    }
    
    fn main() {
        // Инициализирует массив из 3 элементов и задаёт всем значение 42
        let a : [u8; 3] = [42; 3];
        // Альтернативный синтаксис
        // let a = [42u8, 42u8, 42u8];
        for x in a {
            println!("{x}");
        }
        let y = get_index(a.len());
        // Если раскомментировать строку ниже, произойдёт panic
        //println!("{}", a[y]);
    }

Массивы в Rust (продолжение)

  • Массивы можно вкладывать друг в друга
    • В Rust есть несколько встроенных форматировщиков для вывода. В примере ниже :? — это форматировщик debug. Форматировщик :#? используется для pretty print (красивого вывода). Эти форматировщики можно настроить для каждого типа (подробнее об этом позже)
    fn main() {
        let a = [
            [40, 0], // Определяем вложенный массив
            [41, 0],
            [42, 1],
        ];
        for x in a {
            println!("{x:?}");
        }
    }

Кортежи в Rust

  • Кортежи имеют фиксированный размер и могут объединять значения произвольных типов в один составной тип
    • Элементы кортежа индексируются по их позиции (.0, .1, .2, …). Пустой кортеж, то есть (), называется значением unit и соответствует возврату void
    • Rust поддерживает деструктуризацию кортежей, что упрощает привязку переменных к отдельным элементам
fn get_tuple() -> (u32, bool) {
    (42, true)        
}

fn main() {
   let t : (u8, bool) = (42, true);
   let u : (u32, bool) = (43, false);
   println!("{}, {}", t.0, t.1);
   println!("{}, {}", u.0, u.1);
   let (num, flag) = get_tuple(); // Деструктуризация кортежа
   println!("{num}, {flag}");
}

Ссылки в Rust

  • Ссылки в Rust примерно соответствуют указателям в C, но с некоторыми ключевыми отличиями
    • В любой момент времени допустимо иметь любое количество ссылок только для чтения (неизменяемых) на переменную. Ссылка не может пережить область видимости переменной (это ключевое понятие под названием время жизни, которое подробно рассматривается дальше)
    • Для изменяемой переменной допускается только одна изменяемая ссылка для записи, и она не должна пересекаться ни с какими другими ссылками.
fn main() {
    let mut a = 42;
    {
        let b = &a;
        let c = b;
        println!("{} {}", *b, *c); // Компилятор автоматически разыменовывает *c
        
        let d = &mut a;
        
        /*
         * Если раскомментировать строку ниже, программа
         * не скомпилируется, потому что `b` используется,
         * пока изменяемая ссылка `d` активна в текущей области
         * 
         * Нельзя одновременно использовать изменяемую и неизменяемую ссылку
         * в одной области видимости!
         */
        // println!("{}", *b);
    }
    let d = &mut a; // Ок: b и c уже вне области видимости
    *d = 43;
}

Срезы в Rust

  • Ссылки в Rust можно использовать для создания подмножеств массивов
    • В отличие от массивов, которые имеют статическую фиксированную длину, известную на этапе компиляции, срезы могут иметь произвольный размер. Внутри срез реализован как «толстый указатель» (fat pointer), который хранит длину среза и указатель на начальный элемент исходного массива
fn main() {
    let a = [40, 41, 42, 43];
    let b = &a[1..a.len()]; // Срез, начинающийся со второго элемента исходного массива
    let c = &a[1..]; // То же, что и выше
    let d = &a[..]; // То же, что &a[0..] или &a[0..a.len()]
    println!("{b:?} {c:?} {d:?}");
}

Константы и статики в Rust

  • Ключевое слово const используется для определения константного значения. Константы вычисляются на этапе компиляции и встраиваются в программу
  • Ключевое слово static используется для определения аналога глобальных переменных из языков вроде C/C++. Статические переменные имеют адрес в памяти, создаются один раз и существуют на протяжении всего времени работы программы
const SECRET_OF_LIFE: u32 = 42;
static GLOBAL_VARIABLE : u32 = 2;
fn main() {
    println!("The secret of life is {}", SECRET_OF_LIFE);
    println!("Value of global variable is {GLOBAL_VARIABLE}")
}

Строки в Rust: String и &str

  • В Rust два строковых типа, которые служат разным целям
    • String — владеющий, размещённый в куче, растущий (как буфер, выделенный через malloc в C, или std::string в C++)
    • &str — заимствованная лёгкая ссылка (как const char* с длиной в C или std::string_view в C++; но &str проверяется по времени жизни, поэтому никогда не может стать висячим)
    • В отличие от строк с нулевым символом в конце, как в C, строки Rust хранят свою длину и гарантированно являются корректным UTF-8

Для программистов C++: String ≈ std::string, &str ≈ std::string_view. В отличие от std::string_view, проверка заимствований гарантирует, что &str действителен на протяжении всего своего времени жизни.

String и &str: владение и заимствование

Промышленные шаблоны: о том, как работа со строками выглядит вместе с serde в продакшн-коде, см. Обработка JSON: nlohmann::json → serde.

АспектC char*C++ std::stringRust StringRust &str
ПамятьВручную (malloc/free)В куче, владеет буферомВ куче, освобождается автоматическиЗаимствованная ссылка (проверяется по времени жизни)
ИзменяемостьВсегда изменяема через указательИзменяемаИзменяема при наличии mutВсегда неизменяема
Информация о размереОтсутствует (полагается на '\0')Хранит длину и ёмкостьХранит длину и ёмкостьХранит длину (толстый указатель)
КодировкаНе определена (обычно ASCII)Не определена (обычно ASCII)Гарантированно корректный UTF-8Гарантированно корректный UTF-8
Завершающий нульОбязателенОбязателен (c_str())Не используетсяНе используется
fn main() {
    // &str — строковый срез (заимствованный, неизменяемый, обычно строковый литерал)
    let greeting: &str = "Hello";  // Указывает на память только для чтения

    // String — владеющая, размещённая в куче, растущая строка
    let mut owned = String::from(greeting);  // Копирует данные в кучу
    owned.push_str(", World!");        // Увеличиваем строку
    owned.push('!');                   // Добавляем один символ

    // Преобразования между String и &str
    let slice: &str = &owned;          // String -> &str (бесплатно, это просто заимствование)
    let owned2: String = slice.to_string();  // &str -> String (выделяет память)
    let owned3: String = String::from(slice); // То же, что и выше

    // Конкатенация строк (обратите внимание: + поглощает левый операнд)
    let hello = String::from("Hello");
    let world = String::from(", World!");
    let combined = hello + &world;  // hello перемещён (поглощён), world заимствован
    // println!("{hello}");  // Не скомпилируется: hello был перемещён

    // Используйте format!, чтобы избежать проблем с перемещением
    let a = String::from("Hello");
    let b = String::from("World");
    let combined = format!("{a}, {b}!");  // Ни a, ни b не поглощаются

    println!("{combined}");
}

Почему нельзя индексировать строки через []

fn main() {
    let s = String::from("hello");
    // let c = s[0];  // Не скомпилируется! Строки Rust — это UTF-8, а не массивы байтов

    // Безопасные альтернативы:
    let first_char = s.chars().next();           // Option<char>: Some('h')
    let as_bytes = s.as_bytes();                 // &[u8]: сырые байты UTF-8
    let substring = &s[0..1];                    // &str: "h" (диапазон байтов, должен попадать на границу UTF-8)

    println!("First char: {:?}", first_char);
    println!("Bytes: {:?}", &as_bytes[..5]);
}

Упражнение: работа со строками

🟢 Начальный уровень

  • Напишите функцию fn count_words(text: &str) -> usize, которая считает количество слов, разделённых пробельными символами, в строке
  • Напишите функцию fn longest_word(text: &str) -> &str, которая возвращает самое длинное слово (подсказка: придётся подумать о временах жизни — почему возвращаемый тип должен быть &str, а не String?)
Решение (нажмите, чтобы развернуть)
fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

fn longest_word(text: &str) -> &str {
    text.split_whitespace()
        .max_by_key(|word| word.len())
        .unwrap_or("")
}

fn main() {
    let text = "the quick brown fox jumps over the lazy dog";
    println!("Word count: {}", count_words(text));       // 9
    println!("Longest word: {}", longest_word(text));     // "jumps"
}

Структуры в Rust

  • Ключевое слово struct объявляет пользовательский тип структуры
    • Члены struct могут быть именованными или безымянными (кортежные структуры)
  • В отличие от языков вроде C++, в Rust нет понятия «наследования данных»
fn main() {
    struct MyStruct {
        num: u32,
        is_secret_of_life: bool,
    }
    let x = MyStruct {
        num: 42,
        is_secret_of_life: true,
    };
    let y = MyStruct {
        num: x.num,
        is_secret_of_life: x.is_secret_of_life,
    };
    let z = MyStruct { num: x.num, ..x }; // Оператор .. означает: скопировать остальные поля
    println!("{} {} {}", x.num, y.is_secret_of_life, z.num);
}

Кортежные структуры в Rust

  • Кортежные структуры в Rust похожи на кортежи, а их поля не имеют имён
    • Как и в кортежах, к отдельным элементам обращаются через .0, .1, .2, …. Типичный случай использования кортежных структур — оборачивание примитивных типов для создания собственных типов. Это может быть полезно, чтобы не перепутать значения одного и того же типа
struct WeightInGrams(u32);
struct WeightInMilligrams(u32);
fn to_weight_in_grams(kilograms: u32) -> WeightInGrams {
    WeightInGrams(kilograms * 1000)
}

fn to_weight_in_milligrams(w : WeightInGrams) -> WeightInMilligrams  {
    WeightInMilligrams(w.0 * 1000)
}

fn main() {
    let x = to_weight_in_grams(42);
    let y = to_weight_in_milligrams(x);
    // let z : WeightInGrams = x;  // Не скомпилируется: x был перемещён в to_weight_in_milligrams()
    // let a : WeightInGrams = y;   // Не скомпилируется: несоответствие типов (WeightInMilligrams и WeightInGrams)
}

Примечание: атрибут #[derive(...)] автоматически генерирует реализации распространённых трейтов для структур и перечислений. Вы будете встречать его на протяжении всего курса:

#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);           // Debug: работает благодаря #[derive(Debug)]
    let p2 = p.clone();           // Clone: работает благодаря #[derive(Clone)]
    assert_eq!(p, p2);            // PartialEq: работает благодаря #[derive(PartialEq)]
}

Трейтовую систему мы разберём подробно позже, но #[derive(Debug)] настолько полезен, что его стоит добавлять почти к каждой создаваемой вами struct и enum.

Тип Vec в Rust

  • Тип Vec<T> реализует динамический буфер, размещённый в куче (похоже на вручную управляемые массивы через malloc/realloc в C или на std::vector в C++)
    • В отличие от массивов фиксированного размера, Vec может расти и уменьшаться во время выполнения
    • Vec владеет своими данными и автоматически управляет выделением и освобождением памяти
  • Распространённые операции: push(), pop(), insert(), remove(), len(), capacity()
fn main() {
    let mut v = Vec::new();    // Пустой вектор, тип выводится из использования
    v.push(42);                // Добавляем элемент в конец — Vec<i32>
    v.push(43);                
    
    // Безопасная итерация (предпочтительно)
    for x in &v {              // Заимствуем элементы, не поглощая вектор
        println!("{x}");
    }
    
    // Сокращённые способы инициализации
    let mut v2 = vec![1, 2, 3, 4, 5];           // Макрос для инициализации
    let v3 = vec![0; 10];                       // 10 нулей
    
    // Безопасные методы доступа (предпочтительнее индексации)
    match v2.get(0) {
        Some(first) => println!("First: {first}"),
        None => println!("Empty vector"),
    }
    
    // Полезные методы
    println!("Length: {}, Capacity: {}", v2.len(), v2.capacity());
    if let Some(last) = v2.pop() {             // Удаляем и возвращаем последний элемент
        println!("Popped: {last}");
    }
    
    // Опасно: прямая индексация (может вызвать panic!)
    // println!("{}", v2[100]);  // Во время выполнения произойдёт panic
}

Промышленные шаблоны: о безопасных шаблонах .get() из продакшн-кода на Rust см. Избегание неконтролируемой индексации.

Тип HashMap в Rust

  • HashMap реализует поиск по обобщённому ключу -> значению (также известен как dictionary или map)
fn main() {
    use std::collections::HashMap;  // Нужен явный импорт, в отличие от Vec
    let mut map = HashMap::new();       // Выделяем пустой HashMap
    map.insert(40, false);  // Тип выводится как int -> bool
    map.insert(41, false);
    map.insert(42, true);
    for (key, value) in map {
        println!("{key} {value}");
    }
    let map = HashMap::from([(40, false), (41, false), (42, true)]);
    if let Some(x) = map.get(&43) {
        println!("43 was mapped to {x:?}");
    } else {
        println!("No mapping was found for 43");
    }
    let x = map.get(&43).or(Some(&false));  // Значение по умолчанию, если ключ не найден
    println!("{x:?}"); 
}

Упражнение: Vec и HashMap

🟢 Начальный уровень

  • Создайте HashMap<u32, bool> с несколькими записями (убедитесь, что одни значения равны true, а другие — false). Переберите все элементы хеш-таблицы, поместив ключи в один Vec, а значения — в другой
Решение (нажмите, чтобы развернуть)
use std::collections::HashMap;

fn main() {
    let map = HashMap::from([(1, true), (2, false), (3, true), (4, false)]);
    let mut keys = Vec::new();
    let mut values = Vec::new();
    for (k, v) in &map {
        keys.push(*k);
        values.push(*v);
    }
    println!("Keys:   {keys:?}");
    println!("Values: {values:?}");

    // Альтернатива: использовать итераторы с unzip()
    let (keys2, values2): (Vec<u32>, Vec<bool>) = map.into_iter().unzip();
    println!("Keys (unzip):   {keys2:?}");
    println!("Values (unzip): {values2:?}");
}

Углублённый разбор: ссылки C++ и ссылки Rust

Для программистов C++: программисты C++ часто предполагают, что &T в Rust работает так же, как T& в C++. Хотя внешне они похожи, есть принципиальные различия, которые порождают путаницу. Разработчики на C могут пропустить этот раздел — ссылки в Rust рассматриваются в главе Владение и заимствование.

1. Нет rvalue-ссылок и универсальных ссылок

В C++ && имеет два значения в зависимости от контекста:

// C++: && означает разные вещи:
int&& rref = 42;           // Rvalue-ссылка — связывается с временными объектами
void process(Widget&& w);   // Rvalue-ссылка — вызывающий код должен использовать std::move

// Универсальная (forwarding) ссылка — выводится в шаблонном контексте:
template<typename T>
void forward(T&& arg) {     // НЕ rvalue-ссылка! Выводится как T& или T&&
    inner(std::forward<T>(arg));  // Идеальная передача
}

В Rust ничего этого нет. && — это просто оператор логического И.

#![allow(unused)]
fn main() {
// Rust: && — это просто булево И
let a = true && false; // false

// В Rust НЕТ rvalue-ссылок, универсальных ссылок и идеальной передачи.
// Вместо этого:
//   - Перемещение — поведение по умолчанию для типов, не реализующих Copy (std::move не нужен)
//   - Обобщения + ограничения трейтами заменяют универсальные ссылки
//   - Нет различия между временными и постоянными привязками — значения есть значения

fn process(w: Widget) { }      // Принимает владение (как параметр-значение в C++ + неявное перемещение)
fn process_ref(w: &Widget) { } // Заимствует неизменяемо (как const T& в C++)
fn process_mut(w: &mut Widget) { } // Заимствует изменяемо (как T& в C++, но эксклюзивно)
}
Концепция C++Эквивалент в RustПримечания
T& (ссылка на lvalue)&T или &mut TRust разделяет на общие и эксклюзивные
T&& (rvalue-ссылка)Просто TПередача по значению = передача владения
T&& в шаблоне (универсальная ссылка)impl Trait или <T: Trait>Обобщения заменяют передачу
std::move(x)x (просто используйте)Перемещение — поведение по умолчанию
std::forward<T>(x)Эквивалент не нуженНет универсальных ссылок, которые нужно передавать дальше

2. Перемещения — побитовые, конструкторов перемещения нет

В C++ перемещение — это пользовательская операция (конструктор перемещения / оператор присваивания перемещением). В Rust перемещение всегда является побитовым memcpy значения, а исходный объект становится недействительным:

#![allow(unused)]
fn main() {
// Перемещение в Rust = memcpy байтов, исходное значение помечается как недействительное
let s1 = String::from("hello");
let s2 = s1; // Байты s1 копируются в слот стека s2
              // s1 теперь недействителен — компилятор это обеспечивает
// println!("{s1}"); // ❌ Ошибка компиляции: значение использовано после перемещения
}
// Перемещение в C++ = вызов конструктора перемещения (пользовательского!)
std::string s1 = "hello";
std::string s2 = std::move(s1); // Вызывается конструктор перемещения string
// s1 теперь «зомби» в состоянии «допустимо, но не определено»
std::cout << s1; // Компилируется! Выводит... что-то (обычно пустую строку)

Следствия:

  • В Rust нет правила пяти (не нужно определять конструктор копирования, конструктор перемещения, оператор копирующего присваивания, оператор перемещающего присваивания и деструктор)
  • Нет состояния «зомби» после перемещения — компилятор просто запрещает доступ
  • Нет вопросов noexcept для перемещений — побитовое копирование не может выбросить исключение

3. Автоматическое разыменование: компилятор видит сквозь косвенность

Rust автоматически разыменовывает через несколько слоёв указателей и оберток с помощью трейта Deref. В C++ аналога нет:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

// Вложенные обёртки: Arc<Mutex<Vec<String>>>
let data = Arc::new(Mutex::new(vec!["hello".to_string()]));

// В C++ пришлось бы явно разблокировать и вручную разыменовывать на каждом уровне.
// В Rust компилятор автоматически разыменовывает через Arc → Mutex → MutexGuard → Vec:
let guard = data.lock().unwrap(); // Arc автоматически разыменовывается в Mutex
let first: &str = &guard[0];      // MutexGuard→Vec (Deref), Vec[0] (Index),
                                   // &String→&str (приведение через Deref)
println!("First: {first}");

// Вызовы методов тоже автоматически разыменовываются:
let boxed_string = Box::new(String::from("hello"));
println!("Length: {}", boxed_string.len());  // Box→String, затем String::len()
// Не нужно (*boxed_string).len() или boxed_string->len()
}

Приведение через Deref работает и для аргументов функций — компилятор вставляет разыменования, чтобы типы совпали:

fn greet(name: &str) {
    println!("Hello, {name}");
}

fn main() {
    let owned = String::from("Alice");
    let boxed = Box::new(String::from("Bob"));
    let arced = std::sync::Arc::new(String::from("Carol"));

    greet(&owned);  // &String → &str  (1 приведение через Deref)
    greet(&boxed);  // &Box<String> → &String → &str  (2 приведения через Deref)
    greet(&arced);  // &Arc<String> → &String → &str  (2 приведения через Deref)
    greet("Dave");  // Уже &str — приведение не нужно
}
// В C++ для каждого случая потребовались бы .c_str() или явные преобразования.

Цепочка Deref: когда вы вызываете x.method(), разрешение метода в Rust сначала пробует тип получателя T, затем &T, затем &mut T. Если совпадения нет, выполняется разыменование через трейт Deref, и процесс повторяется для целевого типа. Это продолжается через несколько слоёв — поэтому Box<Vec<T>> «просто работает» как Vec<T>. Приведение через Deref (для аргументов функций) — отдельный, но связанный механизм, который автоматически превращает &Box<String> в &str, цепочкой реализаций Deref.

4. Нет нулевых ссылок и нет «опциональных» ссылок

// C++: ссылки не могут быть нулевыми, но указатели могут, и грань между ними размыта
Widget& ref = *ptr;  // Если ptr нулевой → UB
Widget* opt = nullptr;  // «Опциональная» ссылка через указатель
#![allow(unused)]
fn main() {
// Rust: ссылки ВСЕГДА валидны — гарантируется проверкой заимствований
// В безопасном коде нельзя создать нулевую или висячую ссылку
let r: &i32 = &42; // Всегда валидна

// «Опциональная ссылка» задаётся явно:
let opt: Option<&Widget> = None; // Намерение очевидно, нулевого указателя нет
if let Some(w) = opt {
    w.do_something(); // Достижимо только при наличии значения
}
}

5. Ссылки нельзя переназначить

// C++: ссылка — это псевдоним, её нельзя перепривязать
int a = 1, b = 2;
int& r = a;
r = b;  // Это ПРИСВАИВАЕТ значение b переменной a — НЕ перепривязывает r!
// Теперь a равно 2, r по-прежнему ссылается на a
#![allow(unused)]
fn main() {
// Rust: привязки let могут затенять друг друга, но у ссылок свои правила
let a = 1;
let b = 2;
let r = &a;
// r = &b;   // ❌ Нельзя присвоить неизменяемой переменной
let r = &b;  // ✅ Но можно ЗАТЕНИТЬ r новой привязкой
             // Старая привязка исчезает, а не перепривязывается

// С mut:
let mut r = &a;
r = &b;      // ✅ Теперь r указывает на b — это перепривязка (а не присваивание через ссылку)
}

Ментальная модель: в C++ ссылка — это постоянный псевдоним для одного объекта. В Rust ссылка — это значение (указатель с гарантиями времени жизни), которое подчиняется обычным правилам привязки переменных: неизменяемая по умолчанию, перепривязываемая только если объявлена с mut.