Работа с файлами в Rust
Чтение и запись файлов — одно из первых мест, где всё, что ты узнал о Result и операторе ?, перестаёт быть теорией. Почти любая операция с файлом может не получиться (файла нет, нет прав доступа), поэтому стандартная библиотека заставляет разбираться с этим сразу.
Читаем файл целиком
Заголовок раздела «Читаем файл целиком»Самый простой способ прочитать содержимое файла в String:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Не удалось прочитать файл: {e}"), }}fs::read_to_string возвращает Result<String, std::io::Error>: Ok с текстом, если всё получилось, и Err с причиной, если нет (файла нет, нет прав и так далее).
Упрощаем обработку ошибок с помощью ?
Заголовок раздела «Упрощаем обработку ошибок с помощью ?»Писать match для каждой операции с файлом быстро надоедает. Если ты внутри функции, которая сама возвращает Result, оператор ? (со страницы Result) сильно всё упрощает:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // при ошибке сразу возвращает её Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Ошибка: {e}"), }}Записываем в файл
Заголовок раздела «Записываем в файл»use std::fs;
fn main() { let result = fs::write("output.txt", "Привет, файл!");
match result { Ok(()) => println!("Запись прошла успешно"), Err(e) => println!("Не удалось записать: {e}"), }}fs::write создаёт файл, если его нет, а если он есть — полностью перезаписывает его. Помни об этом, если нужно дописать в файл, а не заменить его.
Дописываем в конец файла
Заголовок раздела «Дописываем в конец файла»Чтобы дописать в существующий файл, а не перезаписать его, используй 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(())}Заметь, что здесь сама main возвращает Result. Этот приём стоит знать: когда main возвращает Result, внутри неё можно сразу писать ?, а не заворачивать всё в match.
Читаем по строкам
Заголовок раздела «Читаем по строкам»Большой файл не всегда стоит целиком загружать в память. Вместо этого его можно читать строка за строкой:
use std::fs::File;use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> { let file = File::open("hello.txt")?; let reader = BufReader::new(file);
for line in reader.lines() { println!("{}", line?); }
Ok(())}Выясняем, какая именно ошибка случилась
Заголовок раздела «Выясняем, какая именно ошибка случилась»До сих пор все ошибки обрабатывались одинаково: мы их выводили. Но часто нужно реагировать по-разному в зависимости от того, что пошло не так. Отсутствующий файл — обычно не беда (может, это первый запуск, и можно начать с пустого списка). А «нет доступа» (permission denied) — настоящая проблема, о которой стоит сообщить.
io::Error может назвать свою категорию через .kind(). Этот метод возвращает перечисление ErrorKind (на английском), по которому можно сделать match:
use std::fs;use std::io::ErrorKind;
fn main() { // файл во временной папке системы, которого не существует let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("Загружено строк: {}", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Заметок пока нет, начинаем с чистого листа"), ErrorKind::PermissionDenied => println!("Нет прав на чтение файла с заметками"), _ => println!("Что-то другое пошло не так: {e}"), }, }}Вывод:
Заметок пока нет, начинаем с чистого листаstd::env::temp_dir() возвращает путь к временной папке системы (например, /tmp в Linux), а .join(...) добавляет к нему имя файла. Функции для работы с файлами принимают такой путь так же охотно, как &str.
Всегда оставляй ветку _ в конце: у ErrorKind много вариантов, и со временем добавляются новые. Ветка _ ловит всё, что ты не перечислил.
Практика
Заголовок раздела «Практика»Оба упражнения сначала сами записывают свой файл во временную папку, поэтому работают где угодно, в том числе в Playground.
1. Сохраняем и считаем строки
Заголовок раздела «1. Сохраняем и считаем строки»Запиши в файл три дела через fs::write, по одному на строку. Затем прочитай файл обратно через fs::read_to_string и выведи каждую строку с номером, а в конце — общее количество.
Подсказка: поставь \n между делами, чтобы получились отдельные строки. text.lines() выдаёт строки по одной. Пусть main возвращает std::io::Result<()>, чтобы можно было пользоваться ?.
Показать решение
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "разобраться с владением\nнаписать модуль\nпрочитать файл\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("Всего дел: {count}"); Ok(())}Вывод:
1. разобраться с владением2. написать модуль3. прочитать файлВсего дел: 3fs::write заменяет файл целиком, поэтому сколько ни запускай, получишь те же три строки. Каждый ? возвращает ошибку из main, если запись или чтение не удались.
2. Дописываем в журнал
Заголовок раздела «2. Дописываем в журнал»Создай файл журнала заново со строкой-заголовком, а потом допиши в него ещё две строки через OpenOptions и writeln!. В конце прочитай весь файл и выведи его.
Подсказка: для заголовка используй fs::write (так каждый запуск начинается с чистого файла), а для остальных строк — OpenOptions::new().append(true).open(&path)?. print! (а не println!) не добавит лишнюю пустую строку в конце, ведь файл и так заканчивается переводом строки.
Показать решение
use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== журнал ===\n")?; // начинаем с чистого файла
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "запущено")?; writeln!(file, "завершено")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Вывод:
=== журнал ===запущенозавершеноuse std::fs::{self, OpenOptions}; подключает и сам модуль fs, и OpenOptions. Без первого fs::write каждый запуск продолжал бы дописывать в старый файл, и журнал рос бы бесконечно. Для настоящего журнала это именно то, что нужно, а для упражнения с фиксированным выводом — нет.
fs::read_to_string(path)читает весь файл вStringи возвращаетResult.fs::write(path, contents)записывает файл и перезаписывает его, если он уже есть.OpenOptions::new().append(true)позволяет дописать в файл, а не заменить его.main() -> std::io::Result<()>позволяет писать?прямо вmain, безmatchдля каждого вызова.e.kind()говорит, какая ошибка случилась, напримерErrorKind::NotFound, — и каждую можно обработать по-своему.- Примеры с файлами в папке проекта лучше пробовать локально;
std::env::temp_dir()даёт папку, которая работает где угодно.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что будет, если вызвать fs::write для пути, где файл уже есть?
2. Как дописать в файл, а не перезаписать его?
3. Что даёт то, что main сама возвращает std::io::Result<()>?
4. Что делает оператор ?, если fs::read_to_string(path)? завершился ошибкой?
5. Зачем читать большой файл по строкам через BufReader, а не через fs::read_to_string?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»std::fs— документация стандартной библиотеки (на английском): все функции для файлов и папок, напримерcreate_dir_all,copy,renameиremove_file.- Чтение файла — книга «Язык программирования Rust»: чтение файла в проекте
minigrepиз книги, где эта тема соединяется с аргументами командной строки (перевод на русский). - Файловый ввод-вывод — Rust на примерах: запускаемые примеры открытия, создания и построчного чтения файлов (перевод на русский).
OpenOptions— документация стандартной библиотеки (на английском): все способы открыть файл (дозапись, создание, обрезка и так далее).io::ErrorKind— документация стандартной библиотеки (на английском): полный список категорий ошибок, по которым можно делатьmatch.