std::fs::File и std::ioBufReader и BufWriterstd::path::Path и PathBufstd::fs::copy)std::fs::rename)std::fs::remove_file)Добро пожаловать в одиннадцатую лекцию нашего курса по Rust! Сегодня мы погрузимся в увлекательный мир работы с файлами и вводом-выводом (I/O). Этот раздел критически важен для большинства реальных приложений, будь то чтение конфигурационных файлов, обработка данных или запись логов. Мы начнем с основ, постепенно углубляясь в детали, и закончим практическим упражнением. Лекция рассчитана как на новичков, так и на тех, кто хочет освоить тонкости работы с I/O в Rust.
std::fs::File и std::ioВ Rust ввод-вывод построен вокруг модуля стандартной библиотеки std::io, который предоставляет инструменты для работы с потоками данных, а также модуля std::fs, предназначенного для операций с файловой системой.
std::fs::FileДля работы с файлом его нужно сначала открыть. В Rust это делается с помощью структуры File. Давайте разберем простой пример:
use std::fs::File;
use std::io::Read;
fn main() -> std::io::Result<()> {
// Открываем файл для чтения
let mut file = File::open("example.txt")?;
// Создаем буфер для хранения содержимого
let mut contents = String::new();
// Читаем весь файл в строку
file.read_to_string(&mut contents)?;
println!("Содержимое файла:\n{}", contents);
Ok(())
}
File::open — пытается открыть файл с именем example.txt. Возвращает Result<File, std::io::Error>. Если файл не существует или нет прав доступа, программа завершится с ошибкой.? — оператор обработки ошибок. Если Result содержит Err, функция вернет ошибку, иначе извлечет значение Ok.read_to_string — метод структуры File, который читает весь файл в строку. Требует изменяемую ссылку на String.Если нужно создать файл или перезаписать существующий:
use std::fs::File;
use std::io::Write;
fn main() -> std::io::Result<()> {
let mut file = File::create("output.txt")?;
file.write_all(b"Hello, Rust!")?;
Ok(())
}
File::create — создает новый файл или обрезает существующий.write_all — записывает байты в файл.std::ioМодуль std::io предоставляет трейты и структуры для работы с потоками ввода-вывода:
Read — для чтения данных.Write — для записи данных.Seek — для перемещения курсора в потоке.Эти трейты используются не только с файлами, но и с сетевыми сокетами, стандартным вводом (stdin) и выводом (stdout).
BufReader и BufWriterРабота с файлами напрямую через File может быть неэффективной, особенно при частых операциях чтения или записи. Для оптимизации используются буферизированные обертки: BufReader и BufWriter.
BufReaderBufReader минимизирует прямые системные вызовы, читая данные в буфер:
use std::fs::File;
use std::io::{BufReader, Read};
fn main() -> std::io::Result<()> {
let file = File::open("example.txt")?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
println!("Содержимое:\n{}", contents);
Ok(())
}
BufReader::new — оборачивает File в буферизированный читатель.BufWriterАналогично, 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);
writer.write_all(b"Hello, Rust!")?;
writer.flush()?; // Сбрасываем буфер в файл
Ok(())
}
flush — принудительно записывает содержимое буфера в файл. Без этого данные могут остаться в памяти до завершения программы.Совет: Всегда используйте буферизацию для больших файлов или частых операций I/O.
std::path::Path и PathBufПути в Rust обрабатываются через структуры Path и PathBuf.
PathPath — это неизменяемый срез пути, аналогичный str:
use std::path::Path;
fn main() {
let path = Path::new("/home/user/example.txt");
println!("Расширение: {:?}", path.extension()); // Some("txt")
println!("Имя файла: {:?}", path.file_name()); // Some("example.txt")
println!("Существует? {}", path.exists());
}
Path::new — создает ссылку на путь.extension() и file_name() возвращают Option, так как путь может не содержать этих элементов.PathBufPathBuf — это изменяемая версия пути, аналогичная String:
use std::path::PathBuf;
fn main() {
let mut path = PathBuf::from("/home/user");
path.push("example.txt");
println!("Путь: {:?}", path); // "/home/user/example.txt"
}
push — добавляет компонент к пути с учетом разделителей ОС.Практический совет: Используйте Path для проверки существующих путей, а PathBuf — для построения новых.
Для больших файлов полезно читать их построчно с помощью BufReader:
use std::fs::File;
use std::io::{BufReader, BufRead};
fn main() -> std::io::Result<()> {
let file = File::open("example.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}
lines() — возвращает итератор по строкам файла.Пример записи строк в файл:
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);
writeln!(writer, "Первая строка")?;
writeln!(writer, "Вторая строка")?;
writer.flush()?;
Ok(())
}
File::create("output.txt") всегда заменяет существующий файл, если он уже существует. Это поведение определено в стандартной библиотеке Rust: File::create создает новый файл или обрезает (перезаписывает) существующий файл до пустого состояния перед записью.writeln! — макрос, аналогичный println!, но для записи в поток.Чтобы дописать данные в конец файла, нужно использовать OpenOptions вместо File::create
и указать режим append:
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
fn main() -> std::io::Result<()> {
let file = OpenOptions::new()
.write(true) // Разрешаем запись
.append(true) // Устанавливаем режим дозаписи (append)
.create(true) // Создаем файл, если он не существует
.open("output.txt")?; // Открываем файл
let mut writer = BufWriter::new(file);
writeln!(writer, "Первая строка")?;
writeln!(writer, "Вторая строка")?;
writer.flush()?;
Ok(())
}
std::fs::copy)В Rust используется функция std::fs::copy, которая принимает исходный и целевой пути и возвращает Result<u64, std::io::Error>, где u64 — количество скопированных байтов.
Пример:
use std::fs;
fn main() {
match fs::copy("source.txt", "destination.txt") {
Ok(bytes) => println!("Скопировано {} байт", bytes),
Err(e) => println!("Ошибка при копировании: {}", e),
}
}
fs::copy Копирует файл из source в destination.Result<u64, std::io::Error> — при успехе возвращает размер скопированного файла в байтах.unwrapuse std::fs;
fn main() {
let bytes = fs::copy("source.txt", "destination.txt").unwrap();
println!("Скопировано {} байт", bytes);
}
Чтобы избежать ошибок, можно проверить существование исходного файла:
use std::fs;
use std::path::Path;
fn main() {
let source = "source.txt";
let destination = "destination.txt";
if Path::new(source).exists() {
match fs::copy(source, destination) {
Ok(bytes) => println!("Скопировано {} байт", bytes),
Err(e) => println!("Ошибка: {}", e),
}
} else {
println!("Исходный файл не существует");
}
}
std::fs::rename)В Rust используется функция std::fs::rename, которая принимает исходный и целевой пути и возвращает Result<(), std::io::Error>.
Пример:
use std::fs;
fn main() {
match fs::rename("oldname.txt", "newname.txt") {
Ok(()) => println!("Файл успешно перемещён"),
Err(e) => println!("Ошибка при перемещении: {}", e),
}
}
fs::rename Перемещает файл из oldname в newname или переименовывает его.Result<(), std::io::Error> — при успехе ничего не возвращает (()), при ошибке — описание проблемы.unwrapuse std::fs;
fn main() {
fs::rename("oldname.txt", "newname.txt").unwrap();
println!("Файл перемещён");
}
std::fs::rename работает только в пределах одной файловой системы (как rename в PHP). Если нужно переместить файл между разными дисками или разделами, придётся сначала скопировать, а затем удалить исходный файл вручную:
use std::fs;
fn main() {
let source = "source.txt";
let destination = "/mnt/other_disk/destination.txt";
// Копируем файл
match fs::copy(source, destination) {
Ok(_) => {
// Удаляем исходный файл после успешного копирования
match fs::remove_file(source) {
Ok(()) => println!("Файл успешно перемещён"),
Err(e) => println!("Ошибка при удалении исходного файла: {}", e),
}
}
Err(e) => println!("Ошибка при копировании: {}", e),
}
}
rename в POSIX (и в Rust) использует системный вызов, который не работает между разными файловыми системами. В таких случаях копирование + удаление — стандартный подход.
Создадим файл, скопируем его, а затем переместим копию:
use std::fs;
fn main() {
// Создаём исходный файл
fs::write("original.txt", "Привет, мир!").unwrap();
println!("Создан original.txt");
// Копируем файл
match fs::copy("original.txt", "copy.txt") {
Ok(bytes) => println!("Скопировано {} байт в copy.txt", bytes),
Err(e) => println!("Ошибка копирования: {}", e),
}
// Перемещаем копию
match fs::rename("copy.txt", "moved.txt") {
Ok(()) => println!("copy.txt перемещён в moved.txt"),
Err(e) => println!("Ошибка перемещения: {}", e),
}
}
Вывод:
Создан original.txt
Скопировано 21 байт в copy.txt
copy.txt перемещён в moved.txt
std::fs::remove_file)В Rust удаление файла осуществляется с помощью функции std::fs::remove_file. Она принимает путь к файлу и возвращает результат типа Result<(), std::io::Error>, что позволяет обработать возможные ошибки (например, если файла не существует).
Пример:
use std::fs;
fn main() {
let result = fs::remove_file("example.txt");
match result {
Ok(()) => println!("Файл успешно удалён"),
Err(e) => println!("Ошибка при удалении файла: {}", e),
}
}
fs::remove_file Удаляет файл по указанному пути.Result ВозвращаетOk(()) при успехе или Err с информацией об ошибке.unwrapЕсли вы уверены, что файл существует, и не хотите обрабатывать ошибки вручную, можно использовать .unwrap() (но это не рекомендуется в реальных проектах):
use std::fs;
fn main() {
fs::remove_file("example.txt").unwrap();
println!("Файл удалён");
}
Часто проверяют существование файла перед удалением, для этого в Rust можно использовать std::path::Path:
use std::fs;
use std::path::Path;
fn main() {
let path = "example.txt";
if Path::new(path).exists() {
match fs::remove_file(path) {
Ok(()) => println!("Файл успешно удалён"),
Err(e) => println!("Ошибка: {}", e),
}
} else {
println!("Файл не существует");
}
}
fs::remove_file — для файлов.fs::remove_dir — для пустых директорий.fs::remove_dir_all — для директорий с содержимым.Пример:
use std::fs;
fn main() {
fs::remove_dir_all("folder").unwrap(); // Удаляет папку и всё внутри
println!("Папка удалена");
}
Используйте std::path::PathBuf для динамических путей:
use std::fs;
use std::path::PathBuf;
fn main() {
let path = PathBuf::from("example.txt");
fs::remove_file(&path).unwrap();
println!("Файл удалён");
}
Создадим файл, а затем удалим его:
use std::fs;
fn main() {
// Создаём файл
fs::write("temp.txt", "Временный файл").unwrap();
println!("Файл создан");
// Удаляем файл
match fs::remove_file("temp.txt") {
Ok(()) => println!("Файл temp.txt удалён"),
Err(e) => println!("Ошибка: {}", e),
}
}
Вывод:
Файл создан
Файл temp.txt удалён
В Rust основной инструмент для запуска внешних программ — это модуль std::process, в частности структуры Command и Output. Вот основные варианты:
В Rust для простого запуска используется std::process::Command с методом .output():
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.output()
.expect("Ошибка выполнения команды");
// Вывод в stdout как строка
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Вывод: {}", stdout);
// Код возврата
println!("Код возврата: {}", output.status.code().unwrap_or(-1));
}
Command::new Создаёт объект команды, указывая имя программы (например, ls)..arg() Добавляет аргументы (аналог пробелов в строке команды)..output() Выполняет команду и возвращает структуру Output, содержащую stdout, stderr и код возврата.from_utf8_lossy Преобразует байты вывода в строку, заменяя некорректные UTF-8 символы на �.ls -l на, например, dir.stdout и stderrВ Rust для этого используют Command:
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.arg("nonexistent") // добавим несуществующий файл для ошибки
.output()
.expect("Ошибка выполнения команды");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("stdout: {}", stdout);
println!("stderr: {}", stderr);
println!("Код возврата: {}", output.status.code().unwrap_or(-1));
}
Вывод (пример на Linux):
stdout:
stderr: ls: cannot access 'nonexistent': No such file or directory
Код возврата: 2
output.stdout: Байты стандартного вывода.output.stderr: Байты стандартного потока ошибок.В Rust для вывода результата в консоль используйте .status() вместо .output():
use std::process::Command;
fn main() {
let status = Command::new("ls")
.arg("-l")
.status()
.expect("Ошибка выполнения команды");
println!("Код возврата: {}", status.code().unwrap_or(-1));
}
Вывод автоматически идёт в текущий терминал, но вы не можете захватить его как строку. Используйте .output(), если нужен захват.
Если нужно читать вывод по мере его появления можно настроить Stdio и использовать spawn:
use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader};
fn main() {
let mut child = Command::new("ping")
.arg("google.com")
.arg("-c")
.arg("5")
.stdout(Stdio::piped()) // перенаправляем stdout в канал
.stderr(Stdio::piped()) // перенаправляем stderr в канал
.spawn()
.expect("Ошибка запуска команды");
// Читаем stdout
if let Some(stdout) = child.stdout.take() {
let reader = BufReader::new(stdout);
for line in reader.lines() {
println!("stdout: {}", line.unwrap());
}
}
// Читаем stderr
if let Some(stderr) = child.stderr.take() {
let reader = BufReader::new(stderr);
for line in reader.lines() {
println!("stderr: {}", line.unwrap());
}
}
let status = child.wait().expect("Ошибка ожидания завершения");
println!("Код возврата: {}", status.code().unwrap_or(-1));
}
Stdio::piped(): Перенаправляет вывод в канал для чтения.spawn(): Запускает процесс асинхронно, возвращает Child.BufReader: Читает вывод построчно в реальном времени.Вывод (пример):
stdout: PING google.com (142.250.190.78) 56(84) bytes of data.
stdout: 64 bytes from 142.250.190.78: icmp_seq=1 ttl=117 time=15.2 ms
...
Код возврата: 0
Запустим команду echo и выведем результат:
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("Hello from Rust!")
.output()
.expect("Ошибка выполнения команды");
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Вывод: {}", stdout.trim()); // trim убирает перенос строки
}
Вывод:
Вывод: Hello from Rust!
Command::output() для захвата вывода.Command с Stdio::piped() и spawn для работы с потоками.В Rust для работы с конфигурационными файлами в форматах INI и TOML можно использовать популярные библиотеки из экосистемы crates.io. Я опишу реализацию для обоих форматов с примерами.
Для работы с INI-файлами в Rust можно использовать библиотеку rust-ini. Она проста в использовании и поддерживает основные возможности формата INI.
Добавьте в ваш Cargo.toml:
[dependencies]
rust-ini = "0.19"
use ini::Ini;
use std::fs;
fn main() -> Result<(), Box> {
// Создаем новый объект для работы с INI
let mut config = Ini::new();
// Добавляем глобальные параметры (без секции)
config.with_section(None::)
.set("ServerAliveInterval", "45") // Устанавливаем значение для ключа
.set("Compression", "yes"); // Еще один глобальный параметр
// Добавляем секцию "Database" с параметрами
config.with_section(Some("Database"))
.set("host", "localhost") // Хост базы данных
.set("port", "5432") // Порт как строка
.set("name", "myapp_db"); // Имя базы данных
// Добавляем секцию "App"
config.with_section(Some("App"))
.set("debug", "true") // Режим отладки
.set("log_level", "info"); // Уровень логирования
// Записываем конфигурацию в файл config.ini
config.write_to_file("config.ini")?;
println!("INI-файл успешно создан!");
Ok(())
}
use ini::Ini;
fn main() -> Result<(), Box> {
// Загружаем INI-файл из диска
let config = Ini::load_from_file("config.ini")?;
// Извлекаем значения с указанием секции и ключа, с дефолтными значениями на случай отсутствия
let db_host = config.get_from(Some("Database"), "host").unwrap_or("default_host"); // Хост базы данных
let debug = config.get_from(Some("App"), "debug").unwrap_or("false"); // Режим отладки
let log_level = config.get_from(Some("App"), "log_level").unwrap_or("default"); // Уровень логирования
println!("Database host: {}, Debug: {}, Log level: {}", db_host, debug, log_level);
Ok(())
}
ServerAliveInterval=45
Compression=yes
[Database]
host=localhost
port=5432
name=myapp_db
[App]
debug=true
log_level=info
Плюсы:
Минусы:
Для работы с TOML в Rust чаще всего используется библиотека serde вместе с toml. TOML особенно популярен в Rust, так как это "родной" формат для Cargo.toml.
Добавьте в ваш Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
use serde::{Serialize, Deserialize};
use std::fs;
use toml;
#[derive(Serialize, Deserialize)]
struct Config {
app: AppConfig, // Вложенная структура для секции app
database: DatabaseConfig, // Вложенная структура для секции database
}
#[derive(Serialize, Deserialize)]
struct AppConfig {
debug: bool, // Булево значение для режима отладки
log_level: String, // Строковое значение для уровня логирования
}
#[derive(Serialize, Deserialize)]
struct DatabaseConfig {
host: String, // Хост базы данных
port: u16, // Порт как 16-битное число
name: String, // Имя базы данных
}
fn main() -> Result<(), Box> {
// Создаем экземпляр структуры с данными
let config = Config {
app: AppConfig {
debug: true,
log_level: "info".to_string(),
},
database: DatabaseConfig {
host: "localhost".to_string(),
port: 5432,
name: "myapp_db".to_string(),
},
};
// Преобразуем структуру в строку TOML
let toml_string = toml::to_string(&config)?;
// Записываем строку в файл
fs::write("config.toml", toml_string)?;
println!("TOML-файл успешно создан!");
Ok(())
}
use serde::{Serialize, Deserialize};
use std::fs;
use toml;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
app: AppConfig,
database: DatabaseConfig,
}
#[derive(Serialize, Deserialize, Debug)]
struct AppConfig {
debug: bool,
log_level: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct DatabaseConfig {
host: String,
port: u16,
name: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Читаем содержимое файла в строку
let toml_string = fs::read_to_string("config.toml")?;
// Десериализуем строку в структуру Config
let config: Config = toml::from_str(&toml_string)?;
// Выводим значения из структуры
println!(
"Database host: {}, Debug: {}, Log level: {}",
config.database.host, config.app.debug, config.app.log_level
);
Ok(())
}
[app]
debug = true
log_level = "info"
[database]
host = "localhost"
port = 5432
name = "myapp_db"
Плюсы:
serde.Минусы:
serde. Это более современный и мощный выбор.Для работы с форматами данных нужны внешние библиотеки. Добавьте их в Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.1"
serde (версия 1.0, с фичей "derive") — библиотека для сериализации и десериализации данных в Rust. Фича "derive" позволяет автоматически генерировать реализацию трейтов Serialize и Deserialize для структур и перечислений с помощью макросов.serde_json (версия 1.0) — дополнение к serde, предоставляющее поддержку сериализации и десериализации данных в формат JSON.csv (версия 1.1) — библиотека для чтения и записи данных в формате CSV (comma-separated values), удобна для работы с табличными данными.Эти крейты часто используются вместе для обработки структурированных данных в различных форматах.
Пример чтения CSV-файла:
use std::fs::File;
use csv::Reader;
fn main() -> std::io::Result<()> {
let file = File::open("data.csv")?; // Открываем файл "data.csv" и возвращаем Result, где "?" обрабатывает ошибку, если файл не удалось открыть
let mut rdr = Reader::from_reader(file); // Создаем экземпляр csv::Reader из открытого файла для чтения CSV-данных
for result in rdr.records() {
let record = result?; // Получаем очередную запись (строку) из CSV-файла как Result, где "?" обрабатывает ошибку, если запись не удалось прочитать
println!("{:?}", record);
}
Ok(())
}
Reader::from_reader — создает парсер CSV из любого источника, реализующего Read.Пример чтения JSON с использованием serde_json:
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::BufReader;
#[derive(Debug, Serialize, Deserialize)] // Автоматически реализует трейты Debug, Serialize и Deserialize для структуры или перечисления
struct Config {
name: String,
value: i32,
}
fn main() -> std::io::Result<()> {
let file = File::open("config.json")?;
let reader = BufReader::new(file);
let config: Config = serde_json::from_reader(reader)?;
println!("Конфигурация: {:?}", config);
Ok(())
}
serde — библиотека для сериализации/десериализации.from_reader — парсит JSON из потока.В контексте программирования, особенно в языках, использующих атрибуты или аннотации (например, Rust или Python с библиотеками вроде serde для сериализации/десериализации), указание #[serde(default)] обычно применяется к структуре или полю в структуре. Оно говорит, что если значение для этого поля отсутствует при десериализации (например, в JSON или другом формате данных), то будет использовано значение по умолчанию.
В Rust, например, с библиотекой serde, это работает следующим образом:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Example {
#[serde(default)]
name: String, // Если "name" не указано, будет использовано значение по умолчанию для String, т.е. ""
#[serde(default)]
age: i32, // Если "age" не указано, будет использовано значение по умолчанию для i32, т.е. 0
}
Здесь #[serde(default)] указывает, что для поля будет использовано значение по умолчанию (определяемое типом данных), если оно не предоставлено в входных данных. Если вы хотите задать собственное значение по умолчанию, можно использовать #[serde(default = "function_name")], где function_name — это функция, возвращающая нужное значение.
Пример с кастомным значением:
use serde::{Serialize, Deserialize};
fn default_age() -> i32 {
18
}
#[derive(Serialize, Deserialize)]
struct Example {
#[serde(default)]
name: String,
#[serde(default = "default_age")]
age: i32, // Если "age" не указано, будет использовано 18
}
Допустим, у нас есть config.json:
{"name": "App", "value": 42}
Код для его чтения уже приведен выше.
Простой логгер:
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
fn log_message(msg: &str) -> std::io::Result<()> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open("log.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", msg)?;
writer.flush()?;
Ok(())
}
fn main() -> std::io::Result<()> {
log_message("Программа запущена")?;
Ok(())
}
OpenOptions — позволяет настроить режим открытия файла (например, добавление вместо перезаписи).Копирование с буферизацией:
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
fn copy_file(src: &str, dst: &str) -> std::io::Result<()> {
let src_file = File::open(src)?;
let dst_file = File::create(dst)?;
let mut reader = BufReader::new(src_file);
let mut writer = BufWriter::new(dst_file);
let mut buffer = [0; 1024]; // Буфер 1 КБ
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 { break; } // Конец файла
writer.write_all(&buffer[..bytes_read])?;
}
writer.flush()?;
Ok(())
}
fn main() -> std::io::Result<()> {
copy_file("source.txt", "dest.txt")?;
Ok(())
}
Напишите программу, которая:
use std::fs::File;
use std::io::{BufReader, BufRead};
fn count_words(filename: &str) -> std::io::Result<usize> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
let mut word_count = 0;
for line in reader.lines() {
let line = line?;
let words = line.split_whitespace().count();
word_count += words;
}
Ok(word_count)
}
fn main() -> std::io::Result<()> {
let filename = "example.txt";
match count_words(filename) {
Ok(count) => println!("Количество слов в файле {}: {}", filename, count),
Err(e) => eprintln!("Ошибка: {}", e),
}
Ok(())
}
split_whitespace — разбивает строку на слова, игнорируя лишние пробелы.count — подсчитывает элементы в итераторе.Result и выводятся пользователю.example.txt с произвольным текстом.Мы изучили основы работы с файлами и вводом-выводом в Rust: от открытия файлов до парсинга сложных форматов. Вы научились использовать буферизацию, работать с путями и решать практические задачи. Теперь вы готовы применять эти знания в своих проектах!