🦀 Rust

Métodos de arreglos y colecciones en Rust: guía detallada

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь. У тебя есть огромный стеллаж с книгами (это наш массив или коллекция). Каждая книга стоит на своей полке под определённым номером.

Что ты можешь делать с книгами на стеллаже?

  • Взять книгу с полки №3 (доступ по индексу).
  • Посчитать, сколько всего книг (длина массива).
  • Проверить, есть ли на стеллаже книга "Гарри Поттер" (поиск элемента).
  • Перебрать все книги и найти те, что стоят на полке дольше года (итерация с условием).
  • Отсортировать книги по алфавиту (сортировка).
  • Превратить список книг в список авторов (преобразование).

В программировании мы постоянно работаем с наборами данных: список пользователей, корзина покупок, результаты теста, координаты точек на карте. Методы массивов и коллекций — это готовые инструменты, которые позволяют нам делать все эти операции быстро, красиво и без ошибок.

💡 Важно: В Rust есть два основных типа коллекций: массивы (фиксированный размер, известный во время компиляции) и векторы (динамический размер, может расти). В этой статье мы будем говорить в основном о векторах (Vec<T>), так как они гибче и используются чаще всего.

Теория: как это работает изнутри

Давай заглянем "под капот" Rust и поймём, почему методы коллекций такие мощные.

Владение и заимствование

Rust — уникальный язык. Он не позволяет двум частям программы одновременно менять данные, если это может привести к путанице. Это называется правила владения.

  • Владение (Ownership): У каждого значения в Rust есть ровно один "хозяин" (переменная). Когда хозяин уходит из области видимости, значение удаляется.
  • Заимствование (Borrowing): Мы можем дать другой части программы "почитать" наши данные (&T — неизменяемая ссылка) или "поправить" их (&mut T — изменяемая ссылка), но не одновременно.

Методы коллекций учитывают это. Например, метод iter() даёт нам неизменяемые ссылки на элементы, а iter_mut() — изменяемые.

Итераторы — двигатель методов

Большинство методов коллекций (map, filter, for_each) работают через итераторы. Итератор — это "умный курсор", который проходит по элементам коллекции один за другим.

Когда ты пишешь numbers.iter().filter(|x| x > 5).collect(), Rust не делает никаких лишних копий. Он просто создаёт цепочку итераторов, которые "лениво" (lazy) ждут, пока ты попросишь результат. Это очень эффективно!

Типы коллекций в Rust

Тип Описание Когда использовать
[T; N] Массив фиксированной длины N Когда размер известен и не меняется (например, координаты точки [x, y])
Vec<T> Динамический массив (вектор) Когда нужно добавлять/удалять элементы (список покупок, результаты поиска)
HashMap<K, V> Словарь (ключ -> значение) Когда нужно быстро найти значение по ключу (телефонная книга)
HashSet<T> Множество уникальных значений Когда нужно проверить, есть ли элемент в наборе (список друзей)

Синтаксис в Rust: все формы и варианты

Давай разберём самые популярные методы. Я сгруппировал их по категориям.

1. Создание и базовая работа

Метод Что делает Пример
Vec::new() Создаёт пустой вектор let v: Vec<i32> = Vec::new();
vec![...] Макрос для создания вектора с элементами let v = vec![1, 2, 3];
push() Добавляет элемент в конец v.push(4);
pop() Удаляет последний элемент и возвращает его let last = v.pop();
len() Возвращает длину вектора println!("{}", v.len());
is_empty() Проверяет, пуст ли вектор if v.is_empty() { ... }

2. Доступ к элементам

Метод Что делает Пример
get(index) Безопасный доступ по индексу (возвращает Option) v.get(2) — вернёт Some(&3) или None
[index] Прямой доступ (паника, если индекс вне границ) v[2] — вернёт 3 или вызовет панику
first() / last() Первый/последний элемент v.first() — вернёт Some(&1)

3. Итерация и преобразование

Метод Что делает Пример
iter() Создаёт итератор по неизменяемым ссылкам for x in v.iter() { ... }
iter_mut() Создаёт итератор по изменяемым ссылкам for x in v.iter_mut() { *x += 1; }
map() Преобразует каждый элемент v.iter().map(|x| x * 2)
filter() Оставляет только элементы, удовлетворяющие условию v.iter().filter(|x| x > 2)
collect() Собирает итератор обратно в коллекцию let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
for_each() Выполняет действие для каждого элемента v.iter().for_each(|x| println!("{}", x));

4. Поиск и проверка

Метод Что делает Пример
contains() Проверяет, содержит ли вектор элемент v.contains(&3)
find() Находит первый элемент, удовлетворяющий условию v.iter().find(|x| x > 2)
position() Находит индекс первого подходящего элемента v.iter().position(|x| x == 3)
any() / all() Проверяет, выполняется ли условие хотя бы для одного / для всех v.iter().any(|x| x > 5)

5. Изменение порядка

Метод Что делает Пример
sort() Сортирует вектор на месте v.sort();
reverse() Переворачивает вектор v.reverse();
dedup() Удаляет последовательные дубликаты v.dedup();

Примеры кода — от простого к сложному

Пример 1: Создание и базовые операции

// Создаём пустой вектор, который будет хранить целые числа
let mut numbers: Vec<i32> = Vec::new();

// Добавляем элементы в конец вектора
numbers.push(10);
numbers.push(20);
numbers.push(30);

// Выводим длину вектора
println!("Длина вектора: {}", numbers.len()); // Выведет: 3

// Проверяем, пуст ли вектор
println!("Вектор пуст? {}", numbers.is_empty()); // Выведет: false

// Удаляем последний элемент
let last = numbers.pop();
println!("Последний элемент: {:?}", last); // Выведет: Some(30)

// Теперь в векторе осталось [10, 20]
println!("Вектор после pop: {:?}", numbers);

Пример 2: Безопасный доступ к элементам

let fruits = vec!["яблоко", "банан", "апельсин"];

// Безопасный доступ — используем get()
match fruits.get(1) {
    Some(fruit) => println!("Фрукт под индексом 1: {}", fruit),
    None => println!("Такого индекса нет!"),
}

// Попробуем получить элемент за пределами массива
match fruits.get(10) {
    Some(fruit) => println!("Фрукт: {}", fruit),
    None => println!("Ошибка! Индекс 10 вне границ вектора."),
}

// Прямой доступ через [index] — может вызвать панику!
// Раскомментируй следующую строку, чтобы увидеть панику:
// println!("{}", fruits[10]); // Программа упадёт с ошибкой!

Пример 3: Итерация и изменение элементов

let mut scores = vec![85, 92, 78, 95, 88];

// Проходим по всем элементам и выводим их
println!("Оценки:");
for score in scores.iter() {
    println!("{}", score);
}

// Увеличиваем каждую оценку на 5 баллов (используем iter_mut)
for score in scores.iter_mut() {
    *score += 5; // * — разыменовываем ссылку, чтобы изменить значение
}

println!("Оценки после добавления 5 баллов: {:?}", scores);
// Выведет: [90, 97, 83, 100, 93]

Пример 4: Фильтрация и преобразование (map + filter)

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Оставляем только чётные числа
let even_numbers: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
println!("Чётные числа: {:?}", even_numbers); // [2, 4, 6, 8, 10]

// Увеличиваем каждое число в 3 раза
let tripled: Vec<i32> = numbers.iter().map(|x| x * 3).collect();
println!("Утроенные числа: {:?}", tripled); // [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]

// Комбинируем: берём числа больше 5, умножаем на 2
let result: Vec<i32> = numbers
    .iter()
    .filter(|x| x > 5)
    .map(|x| x * 2)
    .collect();
println!("Числа > 5, умноженные на 2: {:?}", result); // [12, 14, 16, 18, 20]

Пример 5: Поиск элементов

let words = vec!["кот", "собака", "птица", "рыба", "кот"];

// Проверяем, есть ли слово "птица"
if words.contains(&"птица") {
    println!("Слово 'птица' есть в списке!");
}

// Находим первое слово, которое начинается на 'р'
let first_with_r = words.iter().find(|word| word.starts_with('р'));
match first_with_r {
    Some(word) => println!("Первое слово на 'р': {}", word),
    None => println!("Слов на 'р' не найдено"),
}

// Находим индекс первого вхождения слова "кот"
let index_of_cat = words.iter().position(|word| *word == "кот");
match index_of_cat {
    Some(idx) => println!("Первое 'кот' находится на позиции: {}", idx),
    None => println!("'кот' не найден"),
}

Пример 6: Сортировка и удаление дубликатов

let mut numbers = vec![5, 3, 8, 1, 3, 9, 1, 5];

// Сортируем вектор
numbers.sort();
println!("Отсортировано: {:?}", numbers); // [1, 1, 3, 3, 5, 5, 8, 9]

// Удаляем последовательные дубликаты
numbers.dedup();
println!("После удаления дубликатов: {:?}", numbers); // [1, 3, 5, 8, 9]

// Переворачиваем вектор
numbers.reverse();
println!("В обратном порядке: {:?}", numbers); // [9, 8, 5, 3, 1]

Пример 7: Работа с HashMap (словарь)

use std::collections::HashMap;

// Создаём телефонную книгу
let mut phone_book: HashMap<String, String> = HashMap::new();

// Добавляем контакты
phone_book.insert(String::from("Анна"), String::from("+7-123-456-78-90"));
phone_book.insert(String::from("Борис"), String::from("+7-098-765-43-21"));

// Ищем номер Анны
match phone_book.get("Анна") {
    Some(phone) => println!("Номер Анны: {}", phone),
    None => println!("Анна не найдена"),
}

// Проверяем, есть ли контакт "Вася"
if phone_book.contains_key("Вася") {
    println!("Вася есть в книге");
} else {
    println!("Васи нет в книге");
}

// Проходим по всем контактам
for (name, phone) in phone_book.iter() {
    println!("{}: {}", name, phone);
}

Пример 8: Сложная цепочка методов (реальная задача)

// Представь, что у нас есть список студентов с их оценками
let students = vec![
    ("Анна", 85),
    ("Борис", 92),
    ("Вика", 78),
    ("Глеб", 95),
    ("Даша", 88),
    ("Егор", 55),
];

// Задача: найти имена студентов, у которых оценка выше 80,
// отсортировать их по имени и вывести в верхнем регистре
let top_students: Vec<String> = students
    .iter()                                    // Создаём итератор
    .filter(|(_, score)| *score > 80)          // Оставляем только тех, у кого > 80
    .map(|(name, _)| name.to_uppercase())      // Преобразуем имя в верхний регистр
    .collect();                                // Собираем в вектор

// Сортируем результат
let mut top_students = top_students;
top_students.sort();

println!("Лучшие студенты: {:?}", top_students);
// Выведет: ["АННА", "БОРИС", "ГЛЕБ", "ДАША"]

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаешь Python, тебе будет проще. Но Rust — это не Python. Вот ключевые различия:

Операция Python (списки) Rust (векторы) Комментарий
Создание my_list = [1, 2, 3] let my_vec = vec![1, 2, 3]; В Rust нужно указывать тип или использовать макрос vec!
Добавление my_list.append(4) или my_list += [4] my_vec.push(4); В Rust только push()
Длина len(my_list) my_vec.len() Почти одинаково
Доступ по индексу my_list[2] (ошибка, если нет) my_vec[2] (паника, если нет) В Python ошибка, в Rust паника. Лучше использовать get()
Срез (slice) my_list[1:3] &my_vec[1..3] В Rust срез — это ссылка на часть вектора
Фильтрация [x for x in my_list if x > 2] my_vec.iter().filter(|x| x > 2).collect() В Rust более явно, но мощнее
Преобразование [x * 2 for x in my_list] my_vec.iter().map(|x| x * 2).collect() Аналогично, но в Rust нужно собирать результат
Сортировка my_list.sort() my_vec.sort() Почти одинаково
Типизация Динамическая (список может содержать разные типы) Статическая (вектор может содержать только один тип) Главное отличие! В Rust Vec<i32> — только целые числа
⚠️ Важное замечание: В Python ты можешь написать [1, "hello", 3.14]. В Rust так нельзя! Вектор должен содержать элементы одного типа. Если нужно хранить разные типы, используй перечисления (enum) или Box<dyn Trait>.

Типичные ошибки и подводные камни

Ошибка 1: Выход за границы массива

let v = vec![1, 2, 3];
println!("{}", v[5]); // ПАНИКА! Индекс 5 вне границ

Решение: Всегда используй get() для безопасного доступа.

Ошибка 2: Одновременное заимствование

let mut v = vec![1, 2, 3];
let first = &v[0]; // Неизменяемая ссылка
v.push(4);         // Ошибка! Нельзя изменять вектор, пока есть ссылка
println!("{}", first);

Решение: Не держи ссылки на элементы, когда изменяешь вектор. Или используй индексы.

Ошибка 3: Забыл собрать результат

let v = vec![1, 2, 3];
let doubled = v.iter().map(|x| x * 2); // Это итератор, а не вектор!
println!("{:?}", doubled); // Выведет что-то странное

Решение: Всегда вызывай .collect() в конце цепочки методов.

Ошибка 4: Путаница между iter() и into_iter()

let v = vec![1, 2, 3];
let total: i32 = v.into_iter().sum(); // into_iter() забирает владение!
// println!("{:?}", v); // Ошибка! v больше не существует

Решение: Используй iter(), если хочешь сохранить исходный вектор.

Ошибка 5: Сравнение строк

let mut words = vec!["apple", "banana"];
words.sort(); // Ошибка! &str не реализует Ord

Решение: Используй words.sort_by(|a, b| a.cmp(b)); или преобразуй в String.

Лучшие практики и советы профессионалов

  1. Используй get() вместо прямого доступа по индексу. Это безопаснее и делает код устойчивее к ошибкам.
  2. Предпочитай цепочки методов (map, filter) циклам for. Код становится более читаемым и декларативным.
  3. Не забывай про collect(). Итераторы ленивы — без collect() ничего не произойдёт.
  4. Используй Vec::with_capacity() для больших векторов. Это уменьшит количество перераспределений памяти.
  5. Для поиска используй binary_search() на отсортированных векторах. Это O(log n) вместо O(n).
  6. Избегай копирования больших данных. Используй ссылки и заимствование.
  7. Для уникальных элементов используй HashSet. Это быстрее, чем проверка contains() на векторе.
💡 Совет профи: Всегда думай о том, какой тип коллекции лучше всего подходит для твоей задачи. Не используй вектор, если тебе нужен словарь. Не используй массив, если нужен динамический список.

Вопросы для самопроверки

  1. Вопрос: В чём разница между массивом [T; N] и вектором Vec<T>?

    Ответ: Массив имеет фиксированный размер, известный во время компиляции. Вектор может расти и уменьшаться во время выполнения программы.

  2. Вопрос: Что вернёт метод get(), если индекс выходит за границы вектора?

    Ответ: None. get() возвращает Option<&T>, что позволяет безопасно обработать ситуацию.

  3. Вопрос: Почему в Rust нельзя хранить разные типы в одном векторе?

    Ответ: Rust — статически типизированный язык. Компилятор должен знать размер и тип каждого элемента во время компиляции, чтобы правильно выделить память и обеспечить безопасность.

  4. Вопрос: Что делает метод collect()?

    Ответ: Преобразует итератор обратно в коллекцию (например, в Vec, HashMap или String).

  5. Вопрос: В чём разница между iter() и into_iter()?

    Ответ: iter() заимствует элементы (неизменяемые ссылки), а into_iter() забирает владение (потребляет коллекцию).

  6. Вопрос: Как отсортировать вектор в обратном порядке?

    Ответ: Сначала вызвать sort(), затем reverse(). Или использовать sort_by(|a, b| b.cmp(a)).

  7. Вопрос: Что такое "ленивый" итератор?

    Ответ: Итератор, который не выполняет операции, пока не понадобится результат. Это позволяет создавать цепочки методов без лишних вычислений.

  8. Вопрос: Какой метод использовать для безопасного удаления элемента по индексу?

    Ответ: remove(index) — удаляет элемент и сдвигает остальные. swap_remove(index) — удаляет элемент, заменяя его последним (быстрее, но не сохраняет порядок).

  9. Вопрос: Можно ли использовать for цикл для итерации по вектору без iter()?

    Ответ: Да! for x in &v { ... } — это то же самое, что for x in v.iter() { ... }. Rust делает это автоматически.

  10. Вопрос: Что произойдёт, если вызвать pop() на пустом векторе?

    Ответ: Ничего страшного. pop() вернёт None. Вектор останется пустым.

Итог и что изучить дальше

Мы прошли долгий путь! Ты узнал:

  • Что такое коллекции и зачем они нужны (аналогия с библиотекой).
  • Как работают методы коллекций изнутри (владение, заимствование, итераторы).
  • Полный синтаксис всех основных методов с таблицами.
  • 8 рабочих примеров от простого к сложному.
  • Сравнение с Python (теперь ты знаешь, чем Rust отличается).
  • Типичные