Основи Rust: читання з файлів

Ігор Грегорченко

У нашій серії матеріалів ми розглянемо базові основи новомодної мови Rust. А в другій частині циклу на основі вивченого спробуємо написати найпростіші смарт-контракти для таких блокчейн-проєктів, як Solana. У цьому туторіалі буде багато прикладів, мало теорії та швидкий темп просування.

Цей пост — вільний переклад ось цієї оригінальної статті (з нашими доповненнями у місцях, де це здалося потрібним), яку написав Стів Донован. Початок можна знайти ось тут, а зміст усієї серії — ось тут.

Читання з файлів

Наступним важливим кроком на шляху до відкриття наших програм світові є техніка читання файлів.

Згадайте, що expect схожий на unwrap, але видає повідомлення користувача про помилку. У наступній програмі, цілком гарній на вигляд, ми отримаємо кілька помилок. Далі по тексту розберемося, чому:

// file1.rs
use std::env;
use std::fs::File;
use std::io::Read;

fn main() {
    let first = env::args().nth(1).expect("please supply a filename");

    let mut file = File::open(&first).expect("can't open the file");

    let mut text = String::new();
    file.read_to_string(&mut text).expect("can't read the file");

    println!("file had {} bytes", text.len());

}

Це дає такий висновок:

src$ file1 file1.rs
file had 366 bytes
src$ ./file1 frodo.txt
thread 'main' panicked at 'can't open the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.
src$ file1 file1
thread 'main' panicked at 'can't read the file: Error { repr: Custom(Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }) }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Примітка: Запустіть із ключем `RUST_BACKTRACE=1` для отримання зворотного трасування.

Отже, розбираємось: open може не спрацювати в реальному житті, тому що файл не існує або нам не дозволено його читати, а read_to_string може не спрацювати, тому що файл не містить правильного UTF-8. Щоб передбачити цю можливість, можна додатково використовувати read_to_end та помістити вміст у вектор байтів. Для файлів, які не надто великі, читання в один прийом є ефективним і простим.

Якщо ви знаєте щось про роботу з файлами в інших мовах, вам може бути цікаво, як обробляти ситуацію, коли файл закривається. Якби ми реально записали дані у цей файл, його незакриття могло б призвести до втрати даних. Але тут файл автоматично закривається, коли функція завершується і файлова змінна обнуляється.

Отже, тепер ми повинні поговорити про те, що саме повертає File::open. Якщо Option — це значення, яке може містити щось або нічого, то Result — це значення, яке може містити щось або код помилки. Вони обидва розуміються як unwrap (та його двоюрідний брат expect), але вони є абсолютно різними.

Result визначається двома параметрами типу: значення Ok і значення Err. Умовна скринька Result має два відділення, одне з яких позначено Ok, а інше Err. Ось приклад:

fn good_or_bad(good: bool) -> Result<i32,String> {
    if good {
        Ok(42)
    } else {
        Err("bad".to_string())
    }
}

fn main() {
    println!("{:?}",good_or_bad(true));
    //Ok(42)
    println!("{:?}",good_or_bad(false));
    //Err("bad")

    match good_or_bad(true) {
        Ok(n) => println!("Cool, I got {}",n),
        Err(e) => println!("Huh, I just got {}",e)
    }
    // Cool, I got 42

}

Фактичний тип 'error' довільний — багато людей використовують рядки, доки не опанують типи помилок Rust. Це зручний спосіб повернути або одне значення, або інше.

Ця версія функції читання файлів не є аварійною. Вона повертає результат, і саме сторона, що викликає, має вирішити, як обробити потенційну помилку.

// file2.rs
use std::env;
use std::fs::File;
use std::io::Read;
use std::io;

fn read_to_string(filename: &str) -> Result<String,io::Error> {
    let mut file = match File::open(&filename) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut text = String::new();
    match file.read_to_string(&mut text) {
        Ok(_) => Ok(text),
        Err(e) => Err(e),
    }
}

fn main() {
    let file = env::args().nth(1).expect("please supply a filename");

    let text = read_to_string(&file).expect("bad file man!");

    println!("file had {} bytes", text.len());
}

Перший збіг безпечно отримує значення з Ok, яке стає значенням матчингу. Якщо це Err, то повертається помилка, обгорнута Err.

Друга відповідність повертає рядок, обернений в Ok, або знову помилку. Фактичний вміст Ok не має конкретного значення, тому ми ігноруємо його за допомогою оператора _.

Не дуже красиво виглядає, коли більшість функції — це обробник помилок. Go має тенденцію також виявляти цю проблему з великою кількістю явних ранніх повернень або просто ігноруванням помилок.

На щастя, є короткий шлях.

Модуль std::io визначає псевдонім типу io::Result<T>, він такий самий, як Result<T,io::Error>, і його простіше набирати.

fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = File::open(&filename)?;
    let mut text = String::new();
    file.read_to_string(&mut text)?;
    Ok(text)
}

Оператор ? робить майже те саме, що і збіг в File::open — якщо результатом була помилка, то він негайно повертає цю помилку. В іншому випадку він повертає результат Ok. В кінці нам все ще потрібно повернути рядок як результат.

2017 був хорошим роком для Rust, а ? був одним із тих крутих речей, які стали офіційно стабільними. Але все ще можна зустріти застарілий макрос try!, який використовується в старому коді:

fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = try!(File::open(&filename));
    let mut text = String::new();
    try!(file.read_to_string(&mut text));
    Ok(text)
}

Фінальне побажання

Ця частина не тільки про запис файлів, якщо ви помітили. Сьогодні ми також принагідно обговорили, що можна написати абсолютно безпечний Rust-код, який не є потворним і не потребує при цьому винятків.

Проте у наших прикладах є кілька недоліків. Краще використовувати функції: як правило, функції зрозуміліші та простіші в обслуговуванні за умови, що кожній функції відповідає лише одна ідея.

Інша проблема в тому, що ми не так добре опрацьовуємо помилки, як могли б. Наші програми все ще малі, тому ці недоліки не є великою проблемою, але зі зростанням програми буде все важче їх знайти і виправити, щоб привести код у норму.

Тому рекомендується починати рефакторинг на ранній стадії розробки програми, тому що набагато простіше відрефакторити менші обсяги коду. І недарма вище було сказано про функції, тому що подібні логічні цеглинки програми можна легко обробляти окремо.

Далі буде…

Останні статті

Міноборони розширило функціонал мобільного додатку Резерв+

Міністерство оборони України розширило можливість сплати штрафів через мобільний додаток Резерв+. Тепер у ньому можна…

05.09.2025

Побутова техніка Tesla: армовані метали, передові полімери та доступна ціна. Що представлено в лінійках

Втома від техніки, яка швидко зношується, змушує українців переглядати підхід до покупок. Відтепер на перше…

05.09.2025

Код, згенерований інструментами ШІ, створює вдесятеро більше проблем безпеки

Спеціалісти компанії Apiiro, які проаналізували код з десятків тисяч репозиторіїв, виявили, що розробники за допомогою…

05.09.2025

Державна платформа Brave1 оголошує гранти до 100 млн грн для ШІ-розробників

Маркетплейс Brave1 — онлайн-платформа, що об'єднує українських військових та розробників — запускає грантовий конкурс з…

05.09.2025

У Києві пройде перший хакатон з вайб-кодингу: як взяти участь

4-5 жовтня в Києві на Подолі пройде захід Vibecoding Hackathon. Протягом двох днів розробники, користувачі…

05.09.2025

OpenAI запустить платформу для IT-найму та програму сертифікації навичок

OpenAI готується запустити платформу для найму, яка конкуруватиме з LinkedIn. Майбутній сервіс під назвою OpenAI…

05.09.2025