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 славится своей безопасностью, и работа с файлами — не исключение. Вот несколько правил, которые