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

Раздел 3: Основные модули ввода-вывода и файловой системы

Содержание: 1. std::io: Работа с вводом-выводом 2. std::fs: Операции с файловой системой 3. std::path: Управление путями к файлам Практические советы Пример: Комбинирование модулей Упражнение Заключение

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


1. std::io: Работа с вводом-выводом

Модуль std::io предоставляет инструменты для работы с вводом-выводом (I/O) в Rust. Это включает чтение из файлов, запись в них, работу со стандартными потоками (stdin, stdout, stderr) и низкоуровневые операции с потоками данных. Он построен вокруг концепции "потоков" — абстракции, которая позволяет читать или записывать байты последовательно.

Основные компоненты std::io:

Пример: чтение строки из стандартного ввода:

use std::io;

fn main() -> io::Result<()> {
    let mut input = String::new();
    println!("Введите что-нибудь:");
    io::stdin().read_line(&mut input)?; // Читаем строку из stdin
    println!("Вы ввели: {}", input.trim());
    Ok(())
}
    

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

Заметка: Для повышения производительности используйте BufReader или BufWriter вместо прямого чтения/записи. Буферизация снижает количество системных вызовов.

Буферизация в std::io

Пример с буферизацией:

use std::io::{self, BufRead, BufReader};
use std::fs::File;

fn main() -> io::Result<()> {
    let file = File::open("example.txt")?;
    let reader = BufReader::new(file);
    for line in reader.lines() {
        println!("Строка: {}", line?);
    }
    Ok(())
}
    

Здесь BufReader оборачивает File, а метод lines() возвращает итератор по строкам, что удобно и эффективно.


2. std::fs: Операции с файловой системой

Модуль std::fs предоставляет высокоуровневые функции для работы с файлами и директориями. Он построен поверх std::io и упрощает такие задачи, как создание, удаление, чтение и запись файлов.

Основные функции:

Пример: запись и чтение файла:

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    // Записываем данные в файл
    fs::write("output.txt", "Привет, Rust!")?;
    
    // Читаем файл
    let contents = fs::read_to_string("output.txt")?;
    println!("Содержимое: {}", contents);
    Ok(())
}
    

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

Предупреждение: Операции вроде fs::remove_file или fs::remove_dir_all необратимы. Всегда проверяйте пути перед удалением!

Работа с метаданными

std::fs также позволяет получать информацию о файлах:

use std::fs;

fn main() -> std::io::Result<()> {
    let metadata = fs::metadata("output.txt")?;
    println!("Размер файла: {} байт", metadata.len());
    println!("Это директория? {}", metadata.is_dir());
    Ok(())
}
    

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

Модуль std::fs предоставляет не только функции для чтения и записи файлов, но и мощный инструментарий для получения метаданных о файлах и директориях через структуру std::fs::Metadata. Метаданные — это информация о файле или директории, такая как размер, права доступа, время создания или модификации. Этот раздел подробно разберёт, как извлечь и использовать метаданные, с примерами и практическими советами.

Для получения метаданных используется функция fs::metadata, которая принимает путь к файлу или директории и возвращает io::Result. Если файл не существует или доступ к нему ограничен, функция вернёт ошибку.

Основной пример

Давайте начнём с базового примера, который вы уже видели:

use std::fs;

fn main() -> std::io::Result<()> {
    let metadata = fs::metadata("output.txt")?;
    println!("Размер файла: {} байт", metadata.len());
    println!("Это директория? {}", metadata.is_dir());
    Ok(())
}

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

Но это только начало. Структура Metadata предоставляет гораздо больше информации. Давайте разберём все её методы и возможности.

Полный обзор методов std::fs::Metadata

Структура Metadata содержит данные, зависящие от платформы, но Rust предоставляет унифицированный интерфейс для их извлечения. Вот полный список методов и их назначение:

Пример: Полная информация о файле

Давайте напишем программу, которая извлекает и выводит все доступные метаданные:

use std::fs;
use std::io;
use std::time::SystemTime;

fn main() -> io::Result<()> {
    let metadata = fs::metadata("output.txt")?;
    
    // Основные свойства
    println!("Размер: {} байт", metadata.len());
    println!("Это файл? {}", metadata.is_file());
    println!("Это директория? {}", metadata.is_dir());
    
    // Права доступа
    let perms = metadata.permissions();
    println!("Только чтение? {}", perms.readonly());
    
    // Временные метки
    if let Ok(modified) = metadata.modified() {
        println!("Последняя модификация: {:?}", modified);
    }
    if let Ok(accessed) = metadata.accessed() {
        println!("Последний доступ: {:?}", accessed);
    }
    if let Ok(created) = metadata.created() {
        println!("Создан: {:?}", created);
    } else {
        println!("Время создания недоступно");
    }
    
    Ok(())
}

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

Различие между metadata и symlink_metadata

Функция fs::metadata следует за символическими ссылками и возвращает метаданные конечного файла. Если вам нужно получить метаданные самой ссылки, используйте fs::symlink_metadata:

use std::fs;

fn main() -> std::io::Result<()> {
    let meta = fs::symlink_metadata("link.txt")?;
    println!("Это символическая ссылка? {}", meta.is_symlink());
    println!("Размер ссылки: {} байт", meta.len());
    Ok(())
}

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

Платформозависимые особенности

Метаданные зависят от операционной системы и файловой системы:

Пример для Unix:

#[cfg(target_os = "unix")]
use std::os::unix::fs::MetadataExt;

fn main() -> std::io::Result<()> {
    let metadata = fs::metadata("output.txt")?;
    #[cfg(target_os = "unix")]
    {
        println!("Режим прав: {:o}", metadata.mode()); // Восьмеричный формат
        println!("ID владельца: {}", metadata.uid());
    }
    Ok(())
}

Здесь #[cfg] обеспечивает компиляцию кода только на Unix-системах.

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

Пример: Анализ директории

Программа, которая проверяет все файлы в директории и выводит их метаданные:

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    for entry in fs::read_dir(".")? {
        let entry = entry?;
        let path = entry.path();
        let metadata = fs::metadata(&path)?;
        
        println!("Путь: {:?}", path);
        println!("  Размер: {} байт", metadata.len());
        println!("  Тип: {}", if metadata.is_dir() { "директория" } else { "файл" });
        if let Ok(modified) = metadata.modified() {
            println!("  Модифицирован: {:?}", modified);
        }
    }
    Ok(())
}

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

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

Предупреждение: Размер директории (len()) не всегда отражает суммарный размер её содержимого — это зависит от ОС.


3. std::path: Управление путями к файлам

Модуль std::path отвечает за работу с путями к файлам и директориям. Он абстрагирует различия между платформами (например, слэши в Windows и Unix) и предоставляет безопасный способ манипулировать путями.

Основные типы:

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

use std::path::{Path, PathBuf};

fn main() {
    // Создаём путь
    let path = Path::new("/usr/local/bin/rustc");
    
    // Проверяем компоненты
    println!("Расширение: {:?}", path.extension());
    println!("Имя файла: {:?}", path.file_name());
    println!("Существует? {}", path.exists());
    
    // Строим новый путь
    let mut path_buf = PathBuf::from("/tmp");
    path_buf.push("test.txt");
    println!("Новый путь: {:?}", path_buf);
}
    

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

Заметка: Path не проверяет существование пути до вызова методов вроде exists() или использования с fs. Это просто абстракция.


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


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

Программа, которая читает файл по заданному пути и выводит статистику:

use std::fs;
use std::io;
use std::path::Path;

fn file_stats(path: &Path) -> io::Result<(usize, usize, usize)> {
    let contents = fs::read_to_string(path)?;
    let lines = contents.lines().count();
    let words = contents.split_whitespace().count();
    let chars = contents.chars().count();
    Ok((lines, words, chars))
}

fn main() -> io::Result<()> {
    let path = Path::new("example.txt");
    if path.exists() {
        let (lines, words, chars) = file_stats(path)?;
        println!("Строк: {}, Слов: {}, Символов: {}", lines, words, chars);
    } else {
        println!("Файл {:?} не существует", path);
    }
    Ok(())
}
    

Здесь мы используем std::path для проверки пути, std::fs для чтения и std::io для обработки ошибок.


Упражнение

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

  1. Принимает путь к файлу через аргументы командной строки (std::env::args).
  2. Проверяет, существует ли файл, с помощью std::path.
  3. Если файл существует, читает его с помощью std::fs и выводит первые 10 строк с использованием std::io::BufReader.
  4. Обрабатывает все возможные ошибки.

Подсказка: Используйте take(10) с итератором lines() для ограничения вывода.


Заключение

Модули std::io, std::fs и std::path — это фундаментальные инструменты для работы с вводом-выводом и файловой системой в Rust. Они обеспечивают безопасность, гибкость и кроссплатформенность, но требуют внимательного подхода к обработке ошибок и производительности. Освоив их, вы сможете решать широкий круг задач — от простых скриптов до сложных приложений. В следующих разделах мы рассмотрим другие модули, такие как std::collections, чтобы расширить ваш арсенал.

Попробуйте выполнить упражнение или переходите к следующему разделу!