В этом уроке мы соберем маленькую программу учета задач: список задач положим в Vec<String>, описание соберем через String, а количество задач по категориям посчитаем через HashMap
Так коллекции Rust будут не справочником, а рабочим инструментом. Заодно мы увидим, где появляется ownership строк и почему иногда нужно передавать ссылку, а не забирать значение
- Что получится в конце
- Vec: список, который может расти
- Перебор без потери владения
- String: изменяемая строка
- HashMap: ключ и значение
- Мини-практика: счетчик категорий
- Как читать типы коллекций
- Доступ к элементу: индекс или get
- Сортировка Vec
- String и форматирование
- HashMap и владение ключами
- Частые ошибки и порядок проверки
- Мини-практика: удалить выполненную задачу
- Что может быть еще интересно по этой теме
- Что почитать дальше по Rust
Что получится в конце
Создайте проект:
cargo new rust_collections
cd rust_collections
Код src/main.rs:
use std::collections::HashMap;
fn main() {
let mut tasks = Vec::new();
tasks.push(String::from("write lesson"));
tasks.push(String::from("check examples"));
tasks.push(String::from("publish draft"));
for task in &tasks {
println!("Task: {task}");
}
let mut categories = HashMap::new();
categories.insert(String::from("content"), 2);
categories.insert(String::from("publishing"), 1);
println!("Categories: {categories:?}");
}
Запуск:
cargo run
Vec: список, который может расти
Vec<T> хранит несколько значений одного типа. В нашем случае:
let mut tasks = Vec::new();
Rust может вывести тип из последующих push, но часто в учебном коде полезно указать явно:
let mut tasks: Vec<String> = Vec::new();
Добавление:
tasks.push(String::from("write lesson"));
push меняет вектор, поэтому переменная должна быть mut. Если убрать mut, компилятор остановит программу и объяснит, что нельзя изменять неизменяемую переменную
Перебор без потери владения
В цикле мы пишем:
for task in &tasks {
println!("Task: {task}");
}
Символ & важен. Мы перебираем ссылки на элементы, а не забираем владение каждым String. После такого цикла tasks остается доступным
Если написать:
for task in tasks {
println!("{task}");
}
вектор будет перемещен в цикл. После цикла использовать tasks уже нельзя. Иногда это нормально, но для первого урока чаще нужен перебор по ссылке
String: изменяемая строка
String отличается от строкового литерала &str. Литерал вроде "Rust" обычно живет как ссылка на статический текст, а String — владеющая строка, которую можно менять
Пример:
let mut title = String::from("Rust");
title.push_str(" collections");
println!("{title}");
push_str добавляет строковый срез. Если нужно добавить один символ, используйте push:
title.push('!');
Строки в Rust хранят UTF-8. Поэтому индексировать строку как title[0] нельзя. Один видимый символ может занимать несколько байтов, и Rust не дает случайно разрезать строку посередине символа
HashMap: ключ и значение
HashMap<K, V> хранит пары ключ-значение. Для него нужен импорт:
use std::collections::HashMap;
Создание:
let mut categories = HashMap::new();
categories.insert(String::from("content"), 2);
Если вставить значение по тому же ключу повторно, старое значение будет заменено:
categories.insert(String::from("content"), 3);
Чтобы увеличить счетчик, удобен entry:
let counter = categories.entry(String::from("content")).or_insert(0);
*counter += 1;
Здесь or_insert возвращает изменяемую ссылку на значение в map. Звездочка нужна, чтобы изменить число по ссылке
Мини-практика: счетчик категорий
Сделаем список пар: название задачи и категория. Затем посчитаем, сколько задач в каждой категории:
use std::collections::HashMap;
fn main() {
let tasks = vec![
("write lesson", "content"),
("check code", "content"),
("publish page", "publishing"),
];
let mut counters = HashMap::new();
for (_task, category) in tasks {
let count = counters.entry(category).or_insert(0);
*count += 1;
}
println!("{counters:?}");
}
Вывод будет похож на:
{"content": 2, "publishing": 1}
Порядок ключей может отличаться. HashMap не обязан хранить элементы в порядке вставки, поэтому не привязывайте тесты и вывод к конкретной сортировке
Как читать типы коллекций
Vec<String> читается как «вектор строк». HashMap<String, i32> читается как «map, где ключ строка, значение число»
Такие типы сначала выглядят многословно, но они помогают компилятору ловить ошибки. Вы не сможете случайно положить число в Vec<String> или строку туда, где счетчик должен быть i32
Если компилятор не может вывести тип, добавьте аннотацию:
let mut counters: HashMap<String, i32> = HashMap::new();
Аннотация типа — не поражение, а способ сделать намерение явным
Доступ к элементу: индекс или get
К элементу вектора можно обратиться по индексу:
let tasks = vec![String::from("write"), String::from("check")];
println!("{}", tasks[0]);
Но если индекс выйдет за границы, программа упадет. Более спокойный путь — get, который возвращает Option:
match tasks.get(10) {
Some(task) => println!("{task}"),
None => println!("No task with this index"),
}
Так связываются две темы: коллекции и Option. Rust не заставляет всегда писать длинный код, но дает безопасный вариант там, где ошибка ожидаема
Сортировка Vec
Для простых значений можно отсортировать вектор:
let mut numbers = vec![3, 1, 2];
numbers.sort();
println!("{numbers:?}");
Для строк тоже работает:
let mut tasks = vec![
String::from("publish"),
String::from("check"),
String::from("write"),
];
tasks.sort();
Сортировка меняет коллекцию, поэтому нужен mut. Если нужен старый порядок, сначала сделайте отдельный вектор или продумайте структуру данных. Не используйте clone автоматически, если можно обойтись ссылками или другим алгоритмом
String и форматирование
Если нужно собрать строку из частей, часто удобен format!:
let title = "Rust";
let count = 8;
let message = format!("{title}: {count} lessons");
println!("{message}");
format! возвращает String. Это удобно, когда строку нужно передать дальше, сохранить в коллекцию или вернуть из функции
Для простого вывода println! достаточно. Для создания значения используйте format!. Это маленькое различие делает код аккуратнее: одно печатает, другое собирает строку
HashMap и владение ключами
Когда вы вставляете String в HashMap, map становится владельцем ключа:
let key = String::from("content");
let mut counters = HashMap::new();
counters.insert(key, 1);
После insert переменную key использовать нельзя, потому что строка переехала в map. Если ключ нужен дальше, можно вставлять копию через key.clone(), но сначала подумайте, нужна ли эта копия
В учебном коде часто проще использовать строковые литералы &str как ключи. В реальном коде выбор зависит от того, где живут строки и кто ими владеет
Частые ошибки и порядок проверки
Забыли mut перед Vec или HashMap Добавление элементов меняет коллекцию. Нужен let mut
Переместили вектор в for Если хотите использовать коллекцию после цикла, перебирайте &tasks или tasks.iter()
Пытаетесь индексировать String по числу Rust не разрешает text[0], потому что строка в UTF-8 не гарантирует один байт на символ
Ждете стабильный порядок HashMap Не ждите. Если нужен порядок, смотрите в сторону других структур данных
Мини-практика: удалить выполненную задачу
Добавим маленькую операцию удаления из Vec. Если известен индекс, можно использовать remove:
let mut tasks = vec![
String::from("write"),
String::from("check"),
String::from("publish"),
];
let removed = tasks.remove(1);
println!("Removed: {removed}");
println!("{tasks:?}");
После remove(1) задача "check" будет удалена, а элементы справа сдвинутся влево. Это удобно для маленького списка, но важно помнить: индексы после удаления меняются
Для пользовательского ввода безопаснее сначала проверить индекс:
let index = 10;
if index < tasks.len() {
let removed = tasks.remove(index);
println!("Removed: {removed}");
} else {
println!("No task with this index");
}
Так вы не получите panic из-за выхода за границы. Эта проверка выглядит простой, но именно из таких мелочей складывается надежная работа с коллекциями
Что может быть еще интересно по этой теме
Когда использовать Vec, а когда массив? Массив хорош, если размер известен заранее и фиксирован. Vec нужен, когда список может расти или формируется во время работы
Почему String так часто конфликтует с ownership? Потому что String владеет памятью. Когда вы передаете ее без ссылки, владение может переехать
Можно ли хранить разные типы в одном Vec? Обычно нет, Vec<T> хранит один тип. Для разных вариантов часто используют enum
Что дальше после коллекций? Struct и enum. Они помогают превратить набор строк и чисел в понятные доменные типы
Что почитать дальше по Rust
- Ownership и borrowing в Rust простыми словами
- Типы, match, Option и Result в Rust
- Struct, enum и impl в Rust
- Traits и generics в Rust без академического тумана



