Основы 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-код, который не является уродливым и не нуждается при этом в исключениях.

Тем не менее, в наших примерах есть несколько недостатков. Лучше использовать функции: как правило, функции понятнее и проще в обслуживании, при условии, что каждой функции отвечает только одна идея.

Другая проблема в том, что мы не так хорошо обрабатываем ошибки, как могли бы. Наши программки все еще малы, поэтому эти недостатки не являются большой проблемой, но по мере роста программы будет все труднее их найти и исправить, чтобы привести код в норму.

Поэтому рекомендуется начинать рефакторинг на ранней стадии разработки программы, потому что гораздо проще отрефакторить меньшие объемы кода. И выше не зря было сказано про функции, потому что подобные логические кирпичики программы можно запросто обрабатывать по отдельности.

Продолжение следует…

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

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023