Глава 18: Обзор стандартной библиотеки

Раздел 9: Обработка данных

Содержание: 1. std::str: Работа со строками (UTF-8) 2. std::string: Управление String 3. std::vec: Работа с Vec как отдельным модулем 4. std::iter: Итераторы и их использование Практические советы Пример: Комбинирование модулей Упражнение Заключение

В этом разделе мы рассмотрим модули стандартной библиотеки Rust, связанные с обработкой данных: std::str, std::string, std::vec и std::iter. Эти модули предоставляют инструменты для работы со строками, динамическими массивами и итераторами — основными строительными блоками для манипуляции данными в Rust. Лекция подойдёт как новичкам, так и опытным разработчикам, желающим углубить знания. Мы разберём каждый модуль с примерами, нюансами и практическими советами.


1. std::str: Работа со строками (UTF-8)

Модуль std::str предоставляет функции для работы с &str — неизменяемыми строками в формате UTF-8. Это базовый тип для текстовых данных в Rust.

Основные возможности:

Пример: базовые операции со строками:

fn main() {
    let text = "Hello, Rust!";
    
    // Поиск подстроки
    if text.contains("Rust") {
        println!("Найдено: Rust");
    }
    
    // Разбиение строки
    let words: Vec<&str> = text.split_whitespace().collect();
    println!("Слова: {:?}", words); // ["Hello,", "Rust!"]
    
    // Преобразование регистра
    println!("В верхнем: {}", text.to_uppercase()); // HELLO, RUST!
}
    

Комментарии:

Заметка: Все строки в Rust — это валидный UTF-8. Для работы с сырыми байтами используйте &[u8].


2. std::string: Управление String

Модуль std::string определяет тип String — динамическую, мутабельную строку в формате UTF-8, которая владеет своими данными.

Основные методы:

Пример: манипуляции с String:

fn main() {
    let mut s = String::from("Hello");
    
    // Добавление
    s.push('!');
    s.push_str(" Rust");
    println!("После добавления: {}", s); // Hello! Rust
    
    // Удаление
    s.pop();
    println!("После pop: {}", s); // Hello! Rus
    
    // Очистка
    s.truncate(5);
    println!("После truncate: {}", s); // Hello
}
    

Комментарии:

Предупреждение: Методы вроде truncate паникуют, если обрезка нарушает границы UTF-8 символов.


3. std::vec: Работа с Vec как отдельным модулем

Модуль std::vec определяет тип Vec<T> — динамический массив, который мы уже встречали в std::collections. Здесь он рассматривается как самостоятельный модуль с акцентом на его методы и свойства, включая управление памятью.

Основные методы:

Пример: работа с Vec:

fn main() {
    let mut v = Vec::with_capacity(5); // Начальная ёмкость
    
    // Добавление
    v.push(1);
    v.push(2);
    v.insert(1, 3); // Вставка на позицию 1
    println!("Вектор: {:?}", v); // [1, 3, 2]
    
    // Удаление
    let removed = v.remove(0);
    println!("Удалено: {}, Вектор: {:?}", removed, v); // 1, [3, 2]
    
    // Ёмкость
    println!("Длина: {}, Ёмкость: {}", v.len(), v.capacity());
}
    

Комментарии:

Методы управления памятью

capacity: Метод, возвращающий текущую ёмкость вектора — количество элементов, которое Vec может вместить без перевыделения памяти.

fn main() {
    let mut v = Vec::new();
    println!("Ёмкость: {}", v.capacity()); // 0
    
    v.push(1);
    println!("Ёмкость после push: {}", v.capacity()); // Обычно 4 (зависит от реализации)
    
    v.push(2);
    println!("Длина: {}, Ёмкость: {}", v.len(), v.capacity()); // 2, 4
}

Комментарии:

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

fn main() {
    let mut v = Vec::with_capacity(2); // Начальная ёмкость 2
    println!("Начальная ёмкость: {}", v.capacity()); // 2
    
    v.push(1);
    v.push(2);
    println!("Длина: {}, Ёмкость: {}", v.len(), v.capacity()); // 2, 2
    
    v.reserve(5); // Резервируем место для 5 дополнительных элементов
    println!("Ёмкость после reserve: {}", v.capacity()); // >= 7 (2 + 5)
    
    v.push(3); // Без перевыделения
    println!("Длина: {}, Ёмкость: {}", v.len(), v.capacity()); // 3, >=7
}

Комментарии:

Пример: управление памятью в цикле:

fn main() {
    let mut v = Vec::new();
    let iterations = 1000;
    
    // Без резервирования
    let mut v_no_reserve = Vec::new();
    for i in 0..iterations {
        v_no_reserve.push(i);
    }
    println!("Без reserve - Длина: {}, Ёмкость: {}", 
        v_no_reserve.len(), v_no_reserve.capacity()); // 1000, >=1000
    
    // С резервированием
    let mut v_with_reserve = Vec::new();
    v_with_reserve.reserve(iterations); // Заранее выделяем память
    for i in 0..iterations {
        v_with_reserve.push(i);
    }
    println!("С reserve - Длина: {}, Ёмкость: {}", 
        v_with_reserve.len(), v_with_reserve.capacity()); // 1000, >=1000
}

Комментарии:

Заметка: Используйте reserve, если заранее знаете, сколько элементов будет добавлено, чтобы избежать лишних перевыделений.

Предупреждение: reserve не уменьшает ёмкость — для этого используйте shrink_to_fit.


4. std::iter: Итераторы и их использование

Модуль std::iter предоставляет инструменты для работы с итераторами — мощным механизмом для обработки последовательностей данных. Итераторы в Rust ленивы, то есть операции выполняются только при их потреблении (например, с помощью collect или for). Этот раздел подробно разберёт ключевые трейты и методы.

Основные черты:

Пример: использование итераторов:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Фильтрация и преобразование
    let even_squares: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();
    println!("Чётные квадраты: {:?}", even_squares); // [4, 16]
    
    // Сумма
    let sum: i32 = numbers.iter().sum();
    println!("Сумма: {}", sum); // 15
}
    

Комментарии:

Заметка: Используйте into_iter для владения элементами и iter_mut для мутабельных ссылок.

Трейты

Iterator: Основной трейт для итераторов, определяющий поведение последовательности. Любой тип, реализующий Iterator, должен предоставить метод next, который возвращает следующий элемент или None, если элементы закончились.

struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter { count: 0 };
    for num in counter {
        println!("{}", num); // 1, 2, 3, 4, 5
    }
}

Комментарии: Здесь Counter реализует Iterator, возвращая числа от 1 до 5.

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

fn main() {
    let v = vec![1, 2, 3];
    let iter = v.into_iter(); // Vec реализует IntoIterator
    for num in iter {
        println!("{}", num); // 1, 2, 3
    }
}

Комментарии: into_iter забирает владение Vec, в отличие от iter, который даёт ссылки.

Методы

map: Метод итератора, который применяет функцию к каждому элементу, создавая новый итератор с преобразованными значениями.

fn main() {
    let numbers = vec![1, 2, 3];
    let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
    println!("{:?}", squares); // [1, 4, 9]
}

Комментарии: map ленивый — результат вычисляется только при вызове collect.

filter: Метод итератора, который отфильтровывает элементы, оставляя только те, что удовлетворяют предикату.

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).map(|&x| x).collect();
    println!("{:?}", evens); // [2, 4]
}

Комментарии: filter принимает замыкание, возвращающее bool.

collect: Метод итератора, который собирает элементы в коллекцию (например, Vec, String).

fn main() {
    let chars = vec!['a', 'b', 'c'];
    let s: String = chars.into_iter().collect();
    println!("{}", s); // "abc"
}

Комментарии: collect требует, чтобы целевой тип реализовал FromIterator.

Пример: объединение трейтов и методов:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Фильтрация и преобразование
    let even_squares: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();
    println!("Чётные квадраты: {:?}", even_squares); // [4, 16]
}

Комментарии: Здесь Vec реализует IntoIterator, а iter даёт Iterator, который обрабатывается методами.


Практические советы


Пример: Комбинирование модулей

Программа для обработки текста:

fn main() {
    let text = "apple banana apple cherry";
    let mut words = Vec::new();
    
    // Разбиение строки и сбор в Vec
    text.split_whitespace().for_each(|w| words.push(String::from(w)));
    println!("Слова: {:?}", words);
    
    // Подсчёт уникальных слов
    let unique: Vec<String> = words.into_iter()
        .filter(|w| w.len() > 5)
        .collect();
    println!("Уникальные (>5): {:?}", unique);
}
    

Этот пример использует str для разбиения, String для хранения, Vec для коллекции и iter для фильтрации.


Упражнение

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

  1. Принимает строку текста через аргументы командной строки (std::env::args).
  2. Разбивает её на слова с помощью std::str.
  3. Собирает слова в Vec<String> с использованием std::string.
  4. Фильтрует слова длиннее 3 символов и преобразует их в верхний регистр с помощью std::iter.
  5. Выводит результат.

Подсказка: Используйте collect для создания вектора.


Заключение

Модули std::str, std::string, std::vec и std::iter — это основа для обработки данных в Rust. Они обеспечивают гибкость, производительность и безопасность при работе со строками, массивами и последовательностями. Освоив их, вы сможете эффективно манипулировать данными в своих программах. Далее мы рассмотрим работу с ошибками.

Выполните упражнение или двигайтесь дальше!