Читаем и пишем файлы в Rust 1.x: де-факто подходы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Rust и файлы: мифы, страхи и де-факто подходы

Вы когда-нибудь задавались вопросом: «А как же правильно читать и писать файлы в современном Rust?». Если да — вы не одиноки. На Stack Overflow вопрос «What's the de-facto way of reading and writing files in Rust 1.x?» собрал тысячи просмотров и десятки ответов. Это настоящая боль сообщества, особенно для тех, кто пришел из Python или JavaScript, где open() и read() — это база, а в Rust приходится разбираться с владением, заимствованием и кучей трейтов.

В этой статье мы разложим по полочкам все основные способы работы с файлами в Rust 1.x: от «просто прочитать всё в строку» до эффективного построчного чтения больших логов. Вы узнаете, какой подход считается идиоматичным, а какой лучше приберечь для особых случаев. Поехали!

1. std::fs: стандартный арсенал

Модуль std::fs — это ваша первая остановка. Он предоставляет функции для чтения, записи, копирования и удаления файлов. Rust 1.x стабилизировал удобные обёртки, которые скрывают большую часть низкоуровневой работы с системными вызовами.

Чтение всего файла в строку

Самый простой способ — использовать std::fs::read_to_string. Он открывает файл, читает его целиком в память и возвращает Result<String>. Идеально для небольших конфигов или текстовых файлов.

use std::fs;

fn main() -> std::io::Result<()> {
    let content = fs::read_to_string("hello.txt")?;
    println!("Файл содержит:\n{}", content);
    Ok(())
}

Этот способ — де-факто стандарт для простых случаев. Он лаконичен, безопасен и использует ? для обработки ошибок. Единственный минус — весь файл загружается в память, поэтому для гигабайтных логов он не годится.

Чтение в вектор байтов

Если вы работаете с бинарными данными (изображения, архивы), используйте std::fs::read. Он возвращает Vec<u8>.

use std::fs;

fn main() -> std::io::Result<()> {
    let bytes = fs::read("image.png")?;
    println!("Прочитано {} байт", bytes.len());
    Ok(())
}

Запись в файл

Для записи используйте std::fs::write. Он принимает путь и данные (строку или массив байтов) и создаёт/перезаписывает файл.

use std::fs;

fn main() -> std::io::Result<()> {
    fs::write("output.txt", "Hello, Rust!")?;
    Ok(())
}

Просто, как дважды два. Но если вам нужно дописывать в конец файла, а не перезаписывать, придётся использовать OpenOptions.

2. BufReader и BufWriter: когда размер имеет значение

Для больших файлов или частых операций ввода-вывода на помощь приходят буферизированные обёртки. Они уменьшают количество системных вызовов, что критически важно для производительности.

Построчное чтение с BufReader

Классический пример — чтение большого лог-файла построчно:

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

fn main() -> std::io::Result<()> {
    let file = File::open("large_log.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        let line = line?;
        println!("{}", line);
    }
    Ok(())
}

Метод lines() возвращает итератор строк, каждая из которых — Result<String>. Обратите внимание на line? внутри цикла — это обработка ошибок чтения каждой строки.

Важно: lines() удаляет символы перевода строки. Если вам нужно сохранить их, используйте read_until или читайте файл как Vec<u8>.

Запись с BufWriter

При записи больших объёмов данных буферизация также полезна:

use std::fs::File;
use std::io::{BufWriter, Write};

fn main() -> std::io::Result<()> {
    let file = File::create("output.txt")?;
    let mut writer = BufWriter::new(file);

    for i in 0..1000 {
        writeln!(writer, "Строка номер {}", i)?;
    }
    Ok(())
}

Макрос writeln! — это аналог println!, но для записи в объект, реализующий трейт Write. Он автоматически добавляет символ новой строки. Не забудьте импортировать std::io::Write.

3. OpenOptions: полный контроль

Когда вам нужно не просто создать или открыть файл, а, например, дописать в конец, открыть в режиме чтения-записи или создать только если файла нет — используйте OpenOptions.

Дозапись в конец файла

use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open("log.txt")?;

    writeln!(file, "Новая запись в лог")?;
    Ok(())
}

Метод .append(true) устанавливает курсор в конец файла, .create(true) создаёт файл, если его нет. Комбинация флагов даёт гибкость, аналогичную open() из C или Python.

Чтение и запись одновременно

Иногда нужно открыть файл и на чтение, и на запись. Например, для изменения части данных:

use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};

fn main() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .open("data.bin")?;

    // Читаем первые 4 байта
    let mut buf = [0u8; 4];
    file.read_exact(&mut buf)?;
    println!("Прочитано: {:?}", buf);

    // Перемещаемся в конец и дописываем
    file.seek(SeekFrom::End(0))?;
    file.write_all(b"END")?;

    Ok(())
}

Трейты Read, Write и Seek — основа работы с файлами. Их комбинация позволяет делать практически всё, что нужно.

4. Идиоматический Rust: обработка ошибок и владение

Rust славится своей безопасностью, и работа с файлами — не исключение. Вот несколько правил, которые

Блоги

Рекомендаций по книгам