Основи 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-код, який не є потворним і не потребує при цьому винятків.
Проте у наших прикладах є кілька недоліків. Краще використовувати функції: як правило, функції зрозуміліші та простіші в обслуговуванні за умови, що кожній функції відповідає лише одна ідея.
Інша проблема в тому, що ми не так добре опрацьовуємо помилки, як могли б. Наші програми все ще малі, тому ці недоліки не є великою проблемою, але зі зростанням програми буде все важче їх знайти і виправити, щоб привести код у норму.
Тому рекомендується починати рефакторинг на ранній стадії розробки програми, тому що набагато простіше відрефакторити менші обсяги коду. І недарма вище було сказано про функції, тому що подібні логічні цеглинки програми можна легко обробляти окремо.
Далі буде…
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: