Methoden für Arrays und Sammlungen in Rust – eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь. У тебя есть огромный стеллаж с книгами (это наш массив или коллекция). Каждая книга стоит на своей полке под определённым номером.
Что ты можешь делать с книгами на стеллаже?
- Взять книгу с полки №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.
Лучшие практики и советы профессионалов
- Используй
get()вместо прямого доступа по индексу. Это безопаснее и делает код устойчивее к ошибкам. - Предпочитай цепочки методов (map, filter) циклам for. Код становится более читаемым и декларативным.
- Не забывай про
collect(). Итераторы ленивы — без collect() ничего не произойдёт. - Используй
Vec::with_capacity()для больших векторов. Это уменьшит количество перераспределений памяти. - Для поиска используй
binary_search()на отсортированных векторах. Это O(log n) вместо O(n). - Избегай копирования больших данных. Используй ссылки и заимствование.
- Для уникальных элементов используй
HashSet. Это быстрее, чем проверкаcontains()на векторе.
💡 Совет профи: Всегда думай о том, какой тип коллекции лучше всего подходит для твоей задачи. Не используй вектор, если тебе нужен словарь. Не используй массив, если нужен динамический список.
Вопросы для самопроверки
-
Вопрос: В чём разница между массивом
[T; N]и векторомVec<T>?Ответ: Массив имеет фиксированный размер, известный во время компиляции. Вектор может расти и уменьшаться во время выполнения программы.
-
Вопрос: Что вернёт метод
get(), если индекс выходит за границы вектора?Ответ:
None.get()возвращаетOption<&T>, что позволяет безопасно обработать ситуацию. -
Вопрос: Почему в Rust нельзя хранить разные типы в одном векторе?
Ответ: Rust — статически типизированный язык. Компилятор должен знать размер и тип каждого элемента во время компиляции, чтобы правильно выделить память и обеспечить безопасность.
-
Вопрос: Что делает метод
collect()?Ответ: Преобразует итератор обратно в коллекцию (например, в
Vec,HashMapилиString). -
Вопрос: В чём разница между
iter()иinto_iter()?Ответ:
iter()заимствует элементы (неизменяемые ссылки), аinto_iter()забирает владение (потребляет коллекцию). -
Вопрос: Как отсортировать вектор в обратном порядке?
Ответ: Сначала вызвать
sort(), затемreverse(). Или использоватьsort_by(|a, b| b.cmp(a)). -
Вопрос: Что такое "ленивый" итератор?
Ответ: Итератор, который не выполняет операции, пока не понадобится результат. Это позволяет создавать цепочки методов без лишних вычислений.
-
Вопрос: Какой метод использовать для безопасного удаления элемента по индексу?
Ответ:
remove(index)— удаляет элемент и сдвигает остальные.swap_remove(index)— удаляет элемент, заменяя его последним (быстрее, но не сохраняет порядок). -
Вопрос: Можно ли использовать
forцикл для итерации по вектору безiter()?Ответ: Да!
for x in &v { ... }— это то же самое, чтоfor x in v.iter() { ... }. Rust делает это автоматически. -
Вопрос: Что произойдёт, если вызвать
pop()на пустом векторе?Ответ: Ничего страшного.
pop()вернётNone. Вектор останется пустым.
Итог и что изучить дальше
Мы прошли долгий путь! Ты узнал:
- Что такое коллекции и зачем они нужны (аналогия с библиотекой).
- Как работают методы коллекций изнутри (владение, заимствование, итераторы).
- Полный синтаксис всех основных методов с таблицами.
- 8 рабочих примеров от простого к сложному.
- Сравнение с Python (теперь ты знаешь, чем Rust отличается).
- Типичные