sort
)
Сортировка с сохранением ключей (аналог asort
)
Пользовательская сортировка (аналог usort
)
Удаление элемента (аналог unset
)
Сравнение производительности коллекций
Примеры: обработка списков и словарей
Упражнение: Реализовать поиск в HashMap
Добро пожаловать в девятый раздел нашего курса по Rust! Сегодня мы погрузимся в мир коллекций — структур данных, которые позволяют хранить и обрабатывать наборы элементов. Мы разберём основные типы коллекций (Vec
, String
, HashMap
, HashSet
), изучим их методы, операции, итераторы, сравним их производительность и закрепим знания практическими примерами и упражнением. Эта лекция рассчитана как на новичков, так и на тех, кто хочет углубить свои знания, поэтому я постараюсь объяснить всё максимально подробно, шаг за шагом.
Rust предоставляет набор коллекций в стандартной библиотеке (std::collections
), которые решают типичные задачи работы с данными. Коллекции — это не примитивные типы, а структуры, которые используют динамическую память (heap) для хранения данных. Давайте разберём основные из них.
Vec<T>
— Динамический массивVec
(сокращение от "vector") — это изменяемый массив, который может расти или уменьшаться во время выполнения программы. Это одна из самых часто используемых коллекций в Rust.
T
в непрерывной области памяти.[T; N]
с фиксированным размером).String
— СтрокаString
— это изменяемая строка, которая хранит данные в формате UTF-8. Она похожа на Vec<u8>
, но с дополнительной гарантией валидности UTF-8.
Vec
.&str
— это неизменяемый "срез" строки, а String
— полноценный владеющий тип.HashMap<K, V>
— Ассоциативный массивHashMap
— это структура "ключ-значение", где каждому ключу типа K
соответствует значение типа V
. Работает на основе хэширования.
HashSet<T>
— МножествоHashSet
— это коллекция уникальных элементов типа T
, тоже основанная на хэшировании.
Теперь разберём, как работать с этими коллекциями. Я приведу примеры кода, чтобы вы могли сразу попробовать их в действии.
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)
}
push
добавляет элемент в конец (O(1) в среднем, но может быть O(n), если нужно перевыделить память).pop
удаляет и возвращает последний элемент (O(1)).numbers[0]
) паникует, если индекс вне диапазона. Безопасная альтернатива — метод get
(возвращает Option
).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);
}
push_str
добавляет строковый срез (&str
).push
добавляет один символ (char
).clear
очищает строку, но сохраняет выделенную память.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("Боб");
}
insert
добавляет или обновляет пару ключ-значение.get
возвращает Option<&V>
.remove
удаляет элемент по ключу.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");
}
}
insert
добавляет элемент, если его ещё нет.contains
проверяет наличие элемента.iter
, into_iter
, iter_mut
Итераторы — мощный инструмент для работы с коллекциями. Они позволяют обходить элементы, не заботясь о деталях реализации. В Rust есть три основных вида итераторов:
iter
— Заимствование элементовВозвращает итератор по ссылкам (&T
):
&T
) на элементы коллекции.fn main() {
let numbers = vec![1, 2, 3];
for num in numbers.iter() {
println!("Элемент: {}", num);
}
}
Используется для чтения данных без изменения коллекции.
into_iter
— Перемещение элементовПотребляет коллекцию и возвращает итератор по значениям (T
):
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 больше недоступен
}
После этого коллекция уничтожается (перемещается в итератор).
iter_mut
— Изменение элементовВозвращает итератор по изменяемым ссылкам (&mut T
):
&mut T
) на элементы коллекции.
iter
, но ты можешь менять её содержимое через эти ссылки.
fn main() {
let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
*num += 10; // Изменяем элементы
}
println!("Изменённый Vec: {:?}", numbers); // [11, 12, 13]
}
Подходит для модификации коллекции на месте.
Таблица с отличиями между 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 жив |
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
)[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
хранит ключи в отсортированном порядке автоматически. Однако если нужно отсортировать по значениям придётся:
HashMap
в вектор пар (ключ, значение).Пример:
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]
|a, b|
— это замыканиеb.cmp(a)
возвращает порядок: Ordering::Less
, Equal
или Greater
. Инверсия (b.cmp(a) вместо a.cmp(b)
) даёт убывающий порядок.Альтернатива: .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]
.remove()
сдвигает все последующие элементы, что может быть дорого для больших векторов (O(n)).mut
, так как изменяет вектор.Для словарей (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]
Каждая коллекция оптимизирована под свои задачи. Вот краткое сравнение:
Коллекция | Доступ по индексу | 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) при перевыделении памяти.
Vec
хорош для последовательного доступа, но поиск медленный.HashMap
и HashSet
быстры для поиска, но требуют больше памяти из-за хэш-таблицы.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
).
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
вручную.
HashMap
Напишите программу, которая:
HashMap
с именами (строки) и возрастами (целые числа).Вот полный код с пояснениями:
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),
}
}
io::stdin().read_line
читает строку от пользователя.trim()
убирает лишние символы (например, перенос строки).get
возвращает Option
, который мы обрабатываем через match
.Попробуйте запустить код и протестировать с именами "Алиса", "Боб" и "Ева" (последнего нет в словаре).
Vec
для упорядоченных данных, HashMap
для быстрого поиска, HashSet
для уникальности.map
, filter
и collect
для лаконичного кода.Vec::with_capacity
.Option
и match
вместо паники при доступе к данным.Коллекции — это основа работы с данными в Rust. Вы изучили их типы, методы, итераторы и даже написали небольшую программу. Теперь вы готовы применять их в своих проектах!
Экспериментируйте с кодом!. Удачи в изучении Rust!