Vec, String и HashMap в Rust

В этом уроке мы соберем маленькую программу учета задач: список задач положим в Vec<String>, описание соберем через String, а количество задач по категориям посчитаем через HashMap

Так коллекции Rust будут не справочником, а рабочим инструментом. Заодно мы увидим, где появляется ownership строк и почему иногда нужно передавать ссылку, а не забирать значение

Что получится в конце

Создайте проект:

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x