Глава 8: Последовательности: массивы, кортежи и векторы

Содержание: Массивы: фиксированный размер [T; N], инициализация, доступ Кортежи: разнородные данные (T1, T2, ...), деструктуризация Векторы: динамический размер Vec<T>, методы (push, pop) Сравнение производительности и случаев использования Итерация по последовательностям Примеры: обработка массива, кортеж как результат, накопление в Vec Упражнение: Написать программу для обработки списка чисел всеми тремя типами

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

Введение в последовательности

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

  1. Массивы — фиксированные по размеру наборы однотипных данных.
  2. Кортежи — фиксированные по размеру наборы данных, которые могут быть разного типа.
  3. Векторы — динамические списки однотипных данных, которые могут увеличиваться или уменьшаться.

Если представить их как сумки: массивы — это пакет с фиксированным числом одинаковых конфет, кортежи — это ланч-бокс с разными продуктами (бутерброд, яблоко, сок), а векторы — это рюкзак, в который можно добавлять или убирать одинаковые вещи, например, книги.

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


1. Массивы: фиксированный размер [T; N]

Массив в Rust — это структура данных с фиксированным размером, которая хранит элементы одного типа. Его синтаксис: [T; N], где:

Инициализация массивов

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

fn main() {
    // Простой массив из трёх чисел
    let numbers: [i32; 3] = [1, 2, 3];
    
    // Массив с повторяющимся значением (5 элементов, все равны 0)
    let zeros: [i32; 5] = [0; 5];
    
    println!("Numbers: {:?}", numbers); // Вывод: [1, 2, 3]
    println!("Zeros: {:?}", zeros);     // Вывод: [0, 0, 0, 0, 0]
}

Обратите внимание:

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

Доступ к элементам массива осуществляется по индексу (начиная с 0):

fn main() {
    let arr = [10, 20, 30];
    println!("Первый элемент: {}", arr[0]); // 10
    println!("Второй элемент: {}", arr[1]); // 20
}

Но будьте осторожны: попытка обратиться к несуществующему индексу (например, arr[3] для массива из трёх элементов) вызовет панику (ошибку времени выполнения). Позже мы узнаем про безопасные методы доступа.

Особенности


2. Кортежи: разнородные данные (T1, T2, ...)

Кортеж — это структура с фиксированным размером, которая может содержать элементы разных типов. Синтаксис: (T1, T2, ...).

Инициализация кортежей

Кортеж создаётся с помощью круглых скобок:

fn main() {
    let tuple: (i32, f64, &str) = (42, 3.14, "hello");
    println!("Кортеж: {:?}", tuple);
}

Типы элементов могут быть любыми, и их количество фиксировано на этапе компиляции.

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

Доступ осуществляется через точку и индекс (начиная с 0):

fn main() {
    let tuple = (42, 3.14, "hello");
    println!("Первый: {}", tuple.0);  // 42
    println!("Второй: {}", tuple.1);  // 3.14
    println!("Третий: {}", tuple.2);  // "hello"
}

Деструктуризация

Один из самых мощных инструментов работы с кортежами — деструктуризация. Она позволяет "разобрать" кортеж на отдельные переменные:

fn main() {
    let tuple = (42, 3.14, "hello");
    let (num, pi, text) = tuple;
    println!("Число: {}, Пи: {}, Текст: {}", num, pi, text);
}

Если вам нужен только один элемент, остальные можно игнорировать с помощью _:

fn main() {
    let tuple = (42, 3.14, "hello");
    let (num, _, text) = tuple;
    println!("Число: {}, Текст: {}", num, text);
}

Особенности


3. Векторы: динамический размер Vec<T>

Вектор (Vec<T>) — это динамическая структура данных, которая может расти или уменьшаться в размере. Это аналог списков или динамических массивов в других языках.

Инициализация векторов

Векторы создаются с помощью макроса vec! или метода Vec::new():

fn main() {
    // С помощью макроса vec!
    let mut numbers: Vec = vec![1, 2, 3];
    
    // С помощью Vec::new()
    let mut empty: Vec = Vec::new();
    
    println!("Numbers: {:?}", numbers); // [1, 2, 3]
    println!("Empty: {:?}", empty);     // []
}

Обратите внимание на mut: вектор должен быть изменяемым, чтобы добавлять или удалять элементы.

Методы push и pop

fn main() {
    let mut vec = vec![1, 2];
    vec.push(3);                  // Добавляем 3
    println!("После push: {:?}", vec); // [1, 2, 3]
    
    if let Some(value) = vec.pop() {
        println!("Извлечён: {}", value); // 3
    }
    println!("После pop: {:?}", vec);  // [1, 2]
}

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

Как и в массивах, доступ возможен по индексу, но есть безопасный метод get:

fn main() {
    let vec = vec![10, 20, 30];
    println!("Первый: {}", vec[0]);       // 10
    println!("Второй: {}", vec.get(1).unwrap()); // 20 (get возвращает Option)
}

Особенности


4. Сравнение производительности и случаев использования

ТипРазмерТип данныхХранениеПроизводительностьИспользование
МассивФиксированОднотипныеСтекВысокаяСтатические данные
КортежФиксированРазнотипныеСтекВысокаяВозврат нескольких значений
ВекторДинамическийОднотипныеКучаСредняяДинамические списки

5. Итерация по последовательностям

Rust позволяет легко обходить элементы всех трёх типов с помощью циклов.

Итерация по массиву

fn main() {
    let arr = [1, 2, 3];
    for x in arr {
        println!("Элемент: {}", x);
    }
}

Итерация по кортежу

Кортежи напрямую не итерируются, но можно использовать индексы или деструктуризацию для обработки.

Итерация по вектору

fn main() {
    let vec = vec![10, 20, 30];
    for x in &vec { // &vec для неизменяемого доступа
        println!("Элемент: {}", x);
    }
}

Для изменения элементов в векторе используйте iter_mut():

fn main() {
    let mut vec = vec![10, 20, 30];
    for x in vec.iter_mut() {
        *x *= 2; // Удваиваем каждый элемент
    }
    println!("Удвоенный вектор: {:?}", vec); // [20, 40, 60]
}

6. Примеры

Обработка массива

Найдём сумму элементов массива:

fn main() {
    let arr = [1, 2, 3, 4];
    let sum: i32 = arr.iter().sum();
    println!("Сумма массива: {}", sum); // 10
}

.iter() — не только для массивов

Метод .iter() работает не только с массивами, но и с другими типами данных в Rust, которые предоставляют итерацию. Вот краткий комментарий:

Кортеж как результат

Функция возвращает кортеж с результатами:

fn divide(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b) // Целая часть и остаток
}

fn main() {
    let (quotient, remainder) = divide(10, 3);
    println!("Частное: {}, Остаток: {}", quotient, remainder); // 3, 1
}

Накопление в вектор

Соберём чётные числа в вектор:

fn main() {
    let mut vec = Vec::new();
    for i in 0..10 {
        if i % 2 == 0 {
            vec.push(i);
        }
    }
    println!("Чётные числа: {:?}", vec); // [0, 2, 4, 6, 8]
}

7. Упражнение

Задание: Напишите программу, которая:

  1. Принимает список чисел (например, [1, 2, 3, 4, 5]).
  2. Обрабатывает его с использованием массива, кортежа и вектора:
  3. Выводит результаты.

Решение:

fn array_sum(arr: &[i32; 5]) -> i32 {
    arr.iter().sum()
}

fn tuple_min_max(arr: &[i32; 5]) -> (i32, i32) {
    let min = *arr.iter().min().unwrap();
    let max = *arr.iter().max().unwrap();
    (min, max)
}

fn even_numbers(arr: &[i32; 5]) -> Vec {
    let mut vec = Vec::new();
    for &num in arr {
        if num % 2 == 0 {
            vec.push(num);
        }
    }
    vec
}

fn main() {
    let numbers = [1, 2, 3, 4, 5];
    
    // Массив: сумма
    let sum = array_sum(&numbers);
    println!("Сумма: {}", sum); // 15
    
    // Кортеж: минимум и максимум
    let (min, max) = tuple_min_max(&numbers);
    println!("Минимум: {}, Максимум: {}", min, max); // 1, 5
    
    // Вектор: чётные числа
    let evens = even_numbers(&numbers);
    println!("Чётные числа: {:?}", evens); // [2, 4]
}

Разбор:


Заключение

Мы подробно разобрали массивы, кортежи и векторы — три столпа работы с последовательностями в Rust. Теперь вы знаете их синтаксис, особенности, способы итерации и области применения. Практикуйтесь, экспериментируйте с кодом!