Страницы

Поиск по вопросам

воскресенье, 22 декабря 2019 г.

Как покрасивее написать обработку ошибок в Rust?

#rust


Есть примерно вот такой кусок кода парсера сайта с использованием select:

extern crate select;

use select::document::Document;
use select::predicate::{Class, Name, And};

fn main() {
    // Пример; реальность чуть сложнее, но не суть
    let html = "
+15.00
"; let page = Document::from(html); let blog_id: u32 = page.find(And(Name("div"),Class("vote-item"))) .find(Name("span")).first() .unwrap().attr("id") .unwrap().split('_').collect::>().last() .unwrap().parse::().unwrap(); println!("Blog id: {}", blog_id); } Я попытался его переписать так, чтобы на выходе получить Option (который потом переведу в Result; конкретно здесь сохранение точной причины ошибки не интересует). Результат работает, но выглядит, мягко говоря, не очень: let blog_id: Option = page.find(And(Name("div"),Class("vote-item"))).first() .and_then(|x| x.find(Name("span")).first()) .and_then(|x| x.attr("id").and_then(|x| Some(x.to_string()))) .and_then(|x| x.split('_').collect::>().last().and_then(|x| Some(x.to_string()))) .and_then(|x| x.parse::().ok()); match blog_id { Some(i) => println!("Blog id: {}", i), None => println!("Cannot parse blog_id"), }; Возможно ли оформить это как-нибудь более красиво? Если я пытаюсь это как-нибудь упрощать (особенно противный to_string), компилятор незамедлительно начинает ругаться на времена жизни, заимствования и прочую дребедень. Использование match или if let, подозреваю, приведёт к зашкаливающему количеству лесенок, а если их избегать, то красивость всё равно не особо увеличится. Ещё я пытался написать макрос с циклом, эквивалентный постоянным вызовам and_then, но получилось так же некрасиво и to_string никуда не делись. В идеале что-нибудь похоже на такой Python-эквивалент: try: blog_id = int(page.find("div", {"class": "vote-item"}).\ find("span")[0].\ get("id").\ rsplit("_")[-1]) except Exception: blog_id = None (наверно, лучше просто взять xpath, но вопрос пока не про это :)


Ответы

Ответ 1



Если в результате (в возвращаемом значении функции) нужно получить Result, для улучшения читабельности кода можно воспользоваться макросом try!. Любое значение Option, полученное из операций библиотеки select, можно преобразовать в Result методом ok_or, с типом ошибки на ваш выбор, но таким, чтобы он преобразовывался (трейтами Into/From) в тип ошибки для Result, возвращаемого функцией, которая содержит ваш код с использованием try!. fn get_blog_id(page: &Document) -> Result { let item = try!(page.find(And(Name("div"),Class("vote-item"))).first() .ok_or(MyDocError::NoVoteItem))); let id = try!(item.attr("id") .ok_or(MyDocError::NoIdAttr))); ... } Мне непонятно, в чем проблема со временем жизни заимствований в цепочке and_then, но изложенный выше подход позволит этого избежать, поскольку промежуточные значения привязаны к слотам на время вызова содержащей функции и из них можно заимствовать отрезки строк (AKA slices). Возможный недостаток такого подхода в том, что значения-ошибки в параметрах вызовов ok_or создаются в любом случае, так что если тип ошибки имеет нетривиальную инициализацию и/или деструктор (например, создает String), это создает ненужную работу даже в случае успешного выполнения. Чтобы этого избежать, можно воспользоваться ok_or_else и спрятать инициализацию ошибки в лямбда-выражение, но это несколько ухудшит эстетическое качество кода. Добавлено: Библиотеки макросов try_or и try_opt позволяют написать код раннего возврата Option или Result, подобный приведенному выше, более компактно. В последних версиях языка использование стандартного макроса try! можно заменить на оператор ?.

Ответ 2



Начиная с Rust версии 1.22, оператором ? можно пользоваться и для быстрого возврата Option::None: fn get_blog_id(page: &Document) -> Option { let item = page.find(And(Name("div"), Class("vote-item"))).first()?; let id = item.attr("id")?; // ... }

Ответ 3



Сейчас как-то сошлись на использовании макроса hado, который похож на упомянутый в комментариях mdo, но, по мнению некоторых, немножко лучше: hado! { el <- page.find(And(Name("div"),Class("vote-item"))).find(Name("span")).first(); id_s <- el.attr("id"); num_s <- id_s.split('_').last(); num_s.parse::().ok() } По сути он делает то же самое, что и мой код с and_then: для Some вычисляет следующее выражение, а None оставляет как есть, и в результате отдаёт Option — но с более красивым синтаксисом. (Проблем с временем жизни почему-то не возникло.) (Подобно mdo, юзает многочисленные вложенные друг в друга Monad::bind, но пока каких-либо плохих последствий этого не замечено.)

Ответ 4



Хм, unwrap говорите не нравится. Давайте посмотрим что мы имеем. try!: Я давно запреметил один макрос, называется try!, суть его крайне проста. Если есть ошибка то верни Result<..,..> на fn, если ошибки нет верни обьект. Принцип как у unwrap. macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } Описание: https://doc.rust-lang.org/std/macro.try.html Код взят. fn write_to_file_using_try() -> Result<(), MyError> { //Result требуется обязательно реализовывать, так как вы используете try let mut file = try!(File::create("my_best_friends.txt")); //если все ок то верни обьект, если нет, заверши функцию и верни Result try!(file.write_all(b"This is a list of my best friends.")); //если все ок то все ок, если нет, заверши функцию и верни Result println!("I wrote to the file"); Ok(()) } Также есть оператор ? не сталкивалось с ним работать. Prefer using ? syntax to try!. ? is built in to the language and is more succinct than try!. It is the standard method for error propagation. unwrap Думаю вы с ним уже познакомились, его код описывается зачастую так fn unwrap(self) -> T { match self { Option::Some(val) => val, //если обьект относится к enum Option::Some(e) значит он не null и достань значение из него и верни мне Option::None => //если обьект относится к enum Option::None значит он относится к нулу и значения никакого мы не получили panic!("я упал"), } } Есть еще множество способов описания, но они у меня не работают(бо неизведано кто их реализовывает) http://m4rw3r.github.io/rust-questionmark-operator Через IF (работает через что захочется, Option, Result, ...) Перепишу код примера на IF fn write_to_file_using_try() -> bool { if let Ok(mut file) = File::create("my_best_friends.txt") { if let Ok(_e) = file.write_all(b"This is a list of my best friends."){ println!("I wrote to the file"); return true; } } false } Еще пример на IF из моего кода pub fn save(&self) -> bool { { let lock = self.save_file.lock().unwrap(); if let Some(ref file_s) = *lock { if let Ok(file_f) = File::create(file_s){ let mut file = BufWriter::new(file_f); let end_w = [10u8; 1]; let locker = self.arr.lock().unwrap(); for i in locker.iter(){ file.write(i.as_bytes()).unwrap(); file.write(&end_w); } return true; } } } self.warning("no save file..."); false }

Комментариев нет:

Отправить комментарий