Основи 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-код, який не є потворним і не потребує при цьому винятків.

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

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

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

Далі буде…

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

У Росії націоналізували одну з найбільших геймдев-компаній. Звинуватили в підтримці ЗСУ

Таганський суд Москви ухвалив рішення про передачу у власність держави 100% уставного капіталу IT-компанії «Леста…

04.06.2025

Adobe випустила бета-версію Photoshop для Android

Компанія Adobe оголосила про випуск бета-версії мобільного додатку Photoshop для платформи Android. Реліз стався через…

03.06.2025

Користувачам Windows дозволять видалили Microsoft Store і перестануть нав’язувати Edge — але не всім

Microsoft оголосила, що внесе у Windows деякі зміни щодо роботи сторонніх додатків та сервісів. Компанія…

03.06.2025

Salesforce скорочує найм програмістів. Причина в штучному інтелекті

Завдяки інструментам на базі штучного інтелекту американський IT-гігант Salesforce скоротив найм технічних працівників, у тому…

03.06.2025

OpenAI переписує інструмент кодування Codex CLI з TypeScript на Rust

OpenAI переписала свій інструмент кодування Codex CLI з TypeScript на Rust. Причиною названо підвищення продуктивності…

03.06.2025

В Microsoft Bing інтегрували безкоштовний генератор відео від Sora

Microsoft додала відеогенератор Sora від OpenAI у свій мобільний застосунок Bing. Це перший випадок, коли…

03.06.2025