Глава 9: Коллекции в Rust

Содержание: Обзор коллекций: Vec, String, HashMap, HashSet Методы и операции с коллекциями Итераторы: iter, into_iter, iter_mut Сортировка массива с заменой (аналог sort) Сортировка с сохранением ключей (аналог asort) Пользовательская сортировка (аналог usort) Удаление элемента (аналог unset) Сравнение производительности коллекций Примеры: обработка списков и словарей Упражнение: Реализовать поиск в HashMap

Добро пожаловать в девятый раздел нашего курса по Rust! Сегодня мы погрузимся в мир коллекций — структур данных, которые позволяют хранить и обрабатывать наборы элементов. Мы разберём основные типы коллекций (Vec, String, HashMap, HashSet), изучим их методы, операции, итераторы, сравним их производительность и закрепим знания практическими примерами и упражнением. Эта лекция рассчитана как на новичков, так и на тех, кто хочет углубить свои знания, поэтому я постараюсь объяснить всё максимально подробно, шаг за шагом.


1. Обзор коллекций

Rust предоставляет набор коллекций в стандартной библиотеке (std::collections), которые решают типичные задачи работы с данными. Коллекции — это не примитивные типы, а структуры, которые используют динамическую память (heap) для хранения данных. Давайте разберём основные из них.

1.1. Vec<T> — Динамический массив

Vec (сокращение от "vector") — это изменяемый массив, который может расти или уменьшаться во время выполнения программы. Это одна из самых часто используемых коллекций в Rust.

1.2. String — Строка

String — это изменяемая строка, которая хранит данные в формате UTF-8. Она похожа на Vec<u8>, но с дополнительной гарантией валидности UTF-8.

1.3. HashMap<K, V> — Ассоциативный массив

HashMap — это структура "ключ-значение", где каждому ключу типа K соответствует значение типа V. Работает на основе хэширования.

1.4. HashSet<T> — Множество

HashSet — это коллекция уникальных элементов типа T, тоже основанная на хэшировании.


2. Методы и операции с коллекциями

Теперь разберём, как работать с этими коллекциями. Я приведу примеры кода, чтобы вы могли сразу попробовать их в действии.

2.1. Vec<T>

Создание, добавление и доступ к элементам:

fn main() {
    // Создаём пустой Vec
    let mut numbers: Vec<i32> = Vec::new();
    
    // Добавляем элементы
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);
    
    // Доступ по индексу
    println!("Первый элемент: {}", numbers[0]);
    
    // Длина Vec
    println!("Длина: {}", numbers.len());
    
    // Удаляем последний элемент
    let last = numbers.pop();
    println!("Удалённый элемент: {:?}", last); // Some(3)
}

2.2. String

Создание и модификация строки:

fn main() {
    // Создаём пустую строку
    let mut s = String::new();
    
    // Добавляем текст
    s.push_str("Привет, ");
    s.push('R'); // Добавляет один символ
    s.push_str("ust!");
    
    println!("Строка: {}", s); // Привет, Rust!
    
    // Очистка строки
    s.clear();
    println!("После очистки: '{}'", s);
}

2.3. HashMap<K, V>

Работа с парами ключ-значение:

use std::collections::HashMap;

fn main() {
    // Создаём пустой HashMap
    let mut scores = HashMap::new();
    
    // Добавляем элементы
    scores.insert("Алиса", 10);
    scores.insert("Боб", 15);
    
    // Получаем значение
    if let Some(score) = scores.get("Алиса") {
        println!("Очки Алисы: {}", score); // 10
    }
    
    // Удаляем элемент
    scores.remove("Боб");
}

2.4. HashSet<T>

Работа с уникальными элементами:

use std::collections::HashSet;

fn main() {
    let mut set = HashSet::new();
    
    set.insert(1);
    set.insert(2);
    set.insert(1); // Дубликат, не добавится
    
    println!("Размер множества: {}", set.len()); // 2
    
    // Проверяем наличие
    if set.contains(&2) {
        println!("Множество содержит 2");
    }
}

3. Итераторы: iter, into_iter, iter_mut

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

3.1. iter — Заимствование элементов

Возвращает итератор по ссылкам (&T):

fn main() {
    let numbers = vec![1, 2, 3];
    for num in numbers.iter() {
        println!("Элемент: {}", num);
    }
}

Используется для чтения данных без изменения коллекции.

3.2. into_iter — Перемещение элементов

Потребляет коллекцию и возвращает итератор по значениям (T):

Когда ты пишешь for x in коллекция, Rust автоматически вызывает into_iter(), потому что это "по умолчанию" забирает коллекцию. Если хочешь оставить коллекцию, явно используй for x in коллекция.iter().

fn main() {
    let numbers = vec![1, 2, 3];
    for num in numbers.into_iter() {
        println!("Число: {}", num);
    }
    // numbers больше недоступен
}

После этого коллекция уничтожается (перемещается в итератор).

3.3. iter_mut — Изменение элементов

Возвращает итератор по изменяемым ссылкам (&mut T):

fn main() {
    let mut numbers = vec![1, 2, 3];
    for num in numbers.iter_mut() {
        *num += 10; // Изменяем элементы
    }
    println!("Изменённый Vec: {:?}", numbers); // [11, 12, 13]
}

Подходит для модификации коллекции на месте.

3.4 Отличия итераторов

Таблица с отличиями между iter, into_iter и iter_mut в Rust простыми словами:

Характеристика iter into_iter iter_mut
Что возвращает Ссылки (<T>) Сами значения (T) Изменяемые ссылки
(<mut T>)
Можно ли менять элементы Нет, только смотреть Да, но коллекция уже твоя Да, через ссылки
Что с коллекцией Остаётся живой "Исчезает" (перемещается) Остаётся живой
Тип итератора Итератор по ссылкам Итератор по значениям Итератор по изменяемым ссылкам
Когда использовать Хочу посмотреть элементы Хочу забрать элементы Хочу изменить элементы на месте
Требуется ли mut для коллекции Нет Нет (но коллекция уходит) Да, коллекция должна быть mut
Пример кода for x in vec.iter() for x in vec.into_iter() for x in vec.iter_mut()
Пример результата x — ссылка,
vec жив
x — значение,
vec мёртв
x — изменяемая ссылка,
vec жив

3.5 Пример для наглядности

let mut numbers = vec![1, 2, 3];

// iter
for num in numbers.iter() {
    println!("Только смотрю: {}", num); // <i32>
}
println!("numbers после iter: {:?}", numbers); // [1, 2, 3]

// into_iter
for num in numbers.into_iter() {
    println!("Забираю: {}", num); // i32
}
// println!("{:?}", numbers); // Ошибка, numbers перемещён

// iter_mut
let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
    *num += 10; // Меняю через <mut i32>
}
println!("numbers после iter_mut: {:?}", numbers); // [11, 12, 13]

Сортировка массива с заменой (аналог sort)

В Rust массивы фиксированной длины ([T; N]) неизменяемы по размеру, а для динамических коллекций используется Vec<T>. Для сортировки есть метод .sort():
let mut numbers = vec![3, 1, 4, 1, 5];
numbers.sort();
println!("{:?}", numbers); // [1, 1, 3, 4, 5]

Поведение: сортировка идёт по возрастанию, но ключи в Vec не существуют (это просто последовательность элементов).

В Rust нужно объявить mut, чтобы разрешить изменение вектора и требует, чтобы тип элементов реализовал трейт Ord (для сравнения). Для примитивных типов (i32, f64, String) это уже сделано.


Сортировка с сохранением ключей (аналог asort)

Для словарей в Rust используется HashMap или BTreeMap (из std::collections). HashMap не поддерживает встроенную сортировку, так как порядок ключей не гарантирован, но BTreeMap хранит ключи в отсортированном порядке автоматически. Однако если нужно отсортировать по значениям придётся:

Пример:

use std::collections::HashMap;

let mut map: HashMap<&str, i32> = HashMap::from([
    ("b", 3),
    ("a", 1),
    ("c", 2),
]);
let mut sorted: Vec<(&str, i32)> = map.into_iter().collect();
sorted.sort_by(|a, b| a.1.cmp(&b.1)); // сортировка по значению
println!("{:?}", sorted); // [("a", 1), ("c", 2), ("b", 3)]

sorted теперь Vec<(&str, i32)>, а не HashMap, если нужно сохранить как словарь, потребуется дополнительный шаг.

В Rust нет прямого "сортировать на месте с ключами". Нужно создавать новый вектор или вручную преобразовывать данные.


Пользовательская сортировка (аналог usort)

В Rust метод .sort_by() принимает замыкание (closure) для пользовательской сортировки:

let mut numbers = vec![3, 1, 4, 1, 5];
numbers.sort_by(|a, b| b.cmp(a)); // по убыванию
println!("{:?}", numbers); // [5, 4, 3, 1, 1]

Альтернатива: .sort_by_key() для сортировки по вычисляемому ключу:

let mut numbers = vec![3, 1, 4, 1, 5];
numbers.sort_by_key(|&x| -x); // по убыванию через отрицание
println!("{:?}", numbers); // [5, 4, 3, 1, 1]

Rust требует mut и типобезопасности, но замыкания довольно гибкие и мощные


Удаление элемента (аналог unset)

В Rust для Vec используется метод .remove(index):

let mut numbers = vec![1, 2, 3, 4];
numbers.remove(1); // удаляет элемент с индексом 1
println!("{:?}", numbers); // [1, 3, 4]

Для словарей (HashMap):

use std::collections::HashMap;

let mut map: HashMap<&str, i32> = HashMap::from([
    ("a", 1),
    ("b", 2),
    ("c", 3),
]);
map.remove("b"); // удаляет по ключу "b"
println!("{:?}", map); // {"a": 1, "c": 3}

Альтернатива: Если не нужно сдвигать элементы, можно использовать .swap_remove(index) (быстрее, O(1), но меняет порядок):

let mut numbers = vec![1, 2, 3, 4];
numbers.swap_remove(1); // заменяет на последний элемент и удаляет его
println!("{:?}", numbers); // [1, 4, 3]

4. Сравнение производительности коллекций

Каждая коллекция оптимизирована под свои задачи. Вот краткое сравнение:

imm
Коллекция Доступ по индексуВставка Поиск Память
Vec O(1) O(1) аморт.* O(n) Непрерывная
String O(1) (по байтам) O(1) аморт.* O(n) Непрерывная
HashMap O(1) в среднем O(1) в среднем Разбросанная
HashSet O(1) в среднем O(1) в среднем Разбросанная

*Амортизированная O(1) — в среднем быстрая, но может быть O(n) при перевыделении памяти.


5. Примеры: обработка списков и словарей

Пример 1: Фильтрация списка (Vec)

Допустим, нужно отфильтровать чётные числа:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];
    let even: Vec<i32> = numbers.into_iter()
        .filter(|&x| x % 2 == 0)
        .collect();
    println!("Чётные числа: {:?}", even); // [2, 4, 6]
}

Метод collect собирает итератор обратно в коллекцию (например, Vec).

Пример 2: Подсчёт слов (HashMap)

Подсчитаем, сколько раз каждое слово встречается в тексте:

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust world";
    let mut word_count = HashMap::new();
    
    for word in text.split_whitespace() {
        *word_count.entry(word).or_insert(0) += 1;
    }
    
    println!("Подсчёт слов: {:?}", word_count);
    // {"hello": 2, "world": 2, "rust": 1}
}

Метод entry позволяет избежать проверки contains_key вручную.


6. Упражнение: Реализовать поиск в HashMap

Задание

Напишите программу, которая:

  1. Создаёт HashMap с именами (строки) и возрастами (целые числа).
  2. Запрашивает у пользователя имя через консоль.
  3. Выводит возраст человека или сообщение "Не найдено", если имени нет в словаре.

Решение

Вот полный код с пояснениями:

use std::collections::HashMap;
use std::io;

fn main() {
    // 1. Создаём HashMap
    let mut people = HashMap::new();
    people.insert(String::from("Алиса"), 25);
    people.insert(String::from("Боб"), 30);
    people.insert(String::from("Чарли"), 35);
    
    // 2. Запрашиваем имя
    println!("Введите имя для поиска:");
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Ошибка чтения ввода");
    let name = input.trim(); // Убираем \n
    
    // 3. Ищем и выводим результат
    match people.get(name) {
        Some(age) => println!("Возраст {}: {}", name, age),
        None => println!("{} не найдено", name),
    }
}

Разбор

Попробуйте запустить код и протестировать с именами "Алиса", "Боб" и "Ева" (последнего нет в словаре).


Практические советы

  1. Выбор коллекции: Используйте Vec для упорядоченных данных, HashMap для быстрого поиска, HashSet для уникальности.
  2. Итераторы: Комбинируйте map, filter и collect для лаконичного кода.
  3. Память: Если память критична, минимизируйте перевыделения с помощью Vec::with_capacity.
  4. Ошибки: Используйте Option и match вместо паники при доступе к данным.

Заключение

Коллекции — это основа работы с данными в Rust. Вы изучили их типы, методы, итераторы и даже написали небольшую программу. Теперь вы готовы применять их в своих проектах!

Экспериментируйте с кодом!. Удачи в изучении Rust!