std::fs::File
и std::io
BufReader
и BufWriter
std::path::Path
и PathBuf
std::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
.
BufReader
BufReader
минимизирует прямые системные вызовы, читая данные в буфер:
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
.
Path
Path
— это неизменяемый срез пути, аналогичный 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
, так как путь может не содержать этих элементов.PathBuf
PathBuf
— это изменяемая версия пути, аналогичная 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>
— при успехе возвращает размер скопированного файла в байтах.unwrap
use 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>
— при успехе ничего не возвращает (()
), при ошибке — описание проблемы.unwrap
use 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: от открытия файлов до парсинга сложных форматов. Вы научились использовать буферизацию, работать с путями и решать практические задачи. Теперь вы готовы применять эти знания в своих проектах!