Xử lý errors ở Rust thế nào ?
errors
2
Rust
27
White

Giang Nguyen viết ngày 23/03/2017

Type Result

Type trên dùng để làm gì? Result được dùng cho những trường hợp chúng ta muốn
return lại một giá trị nào đó (Ok) hoặc propagating errors (Err). Ví dụ

fn parse(input: String) -> Result(i32, String) {
  Err("parse error".to_string())
}

Trong những ví dụ ví dụ tiếp theo mình sẽ dùng type String để cho đơn giản, thay
vì phải dùng &str nó dính tới lifetime nhiều.

Thật ra thì cái Result trên là một enum gồm 2 variants khác nhau:

enum Result<T, E> {
  Ok(T),
  Err(E)
}

Default, thì Result::* sẽ được import default bởi std, nên thay vì bạn phải dùng

Result::Err(E)
// or
Result::Ok(T)

thì bạn có thể dùng trực tiếp

Err(E)
// or
Ok(T)

Vậy xử lý errors thế nào ?

Panic

Khi một function return trả về là kiểu Result thì chúng ta có thể hoàn toàn lấy
cái giá trị T kia bằng cách nhanh nhất là unwrap.

fn parse(input: String) -> Result<i32, String> {
    Err("parse error".to_string())
}
fn main() {
    let five = parse("5".to_string()).unwrap();

    println!("{}", five);
}

Khi bạn run đoạn code trên bạn có biết chuyện gì sẽ xảy ra không ? Bump program
của bạn bị crash

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "parse error"', /buildslave/rust-buildbot/slave/stable-dist-rustc-linux/build/src/libcore/result.rs:868
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Vì cái hàm unwrap kia:

Panics if the value is an Err, with a panic message provided by the Err's value.
    pub fn unwrap(self) -> T {
        match self {
            Ok(t) => t,
            Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", e),
        }
    }

Unwrap nó sẽ cố gắng match cái T của bạn nếu không có T thì nó match tiếp Err
nếu có error nó panic ra luôn.

Vậy! có nên dùng cái hàm này không? Mình sẽ không giải thích ở đây cuối bài mình
sẽ nói về trường hợp nên dùng nó :)

Pattern matching Result

Nếu có nhìn qua code unwrap thì nó dùng pattern matching để tìm giá trị T cuối
cùng, cho nên chúng ta cũng dùng được pattern matching

fn parse(input: String) -> Result<i32, String> {
    Err("parse error".to_string())
}
fn main() {
    let five = parse("5".to_string());

    match five {
        Ok(item) => println!("{}", item),
        Err(e) => println!("error: {}", e)
    }
}

Nếu run đoạn code trên chúng ta sẽ thấy ở output là đoạn text:

error: parse error

Với cách này thì chúng ta đã ngăn chặn được panic làm chương trình crash.

Dùng if let

Giống như dùng pattern matching, nhưng ở đây dùng if let

fn parse(input: String) -> Result<i32, String> {
    Err("parse error".to_string())
}
fn main() {
    let five = parse("5".to_string());

    if let Ok(item) = five {
        println!("{}", item);
    } else {
        println!("error");
    }
}

Output đoạn code trên là giá trị trong else

error

Detect cái Result kìa là ok hay error

2 functions sau dùng để detect nó thuộc variants nào trong enum Result

pub fn is_ok(&self) -> bool;
pub fn is_err(&self) -> bool;

Convert sang Option

có 2 kiểu convert là convert cái T kia sang Option và convert cái Err sang
Option

pub fn ok(self) -> Option<T>;
pub fn err(self) -> Option<E>

chú ý ở đây 2 fn trên nó sẽ consume luôn chính nó, fn ok(self) nó sẽ bỏ qua
trường hợp lỗi, còn fn err(self) thì nó sẽ bỏ qua trường hợp có tồn tại T. Ví dụ

let x: Result<i32, String> = Ok(32);

assert_eq!(None, x.err());

// or
let x: Result<i32, String> = Err("Some error".to_string());
assert_eq(Some("Some error"), x.err());

Create new value

Chúng ta có thể một Result chứa giá trị mới bằng cách dùng fn map

pub fn map<U, F>(self, opt: F) -> Result<U, E> where F: FnOnce(T) -> U

Nếu bạn là người mới tìm hiểu qua thì có lẻ nhìn vào code sign của fn trên sẽ
hơi khó hiểu nhưng nó cực kỳ đơn giản. Hàm trên nó sẽ nhận input là opt để
produce một Result chứa value U nhưng vẫn giữ nguyên E, opt có type là generic,
generic này là một function sẽ consume T để return về U.

fn parse(input: String) -> Result<i32, String> {
    Ok(10)
}
fn main() {
    let five = parse("5".to_string());

    let dobule = five.map(|x| x * 2);

    match dobule {
        Ok(item) => println!("{}", item),
        Err(e) =>  println!("error: {}", e)
    }

    //output: 10
}

Ngoài ra type Result có khá nhiều built-in function rất hữu ít như: and,
and_then, map_err, or ... Bạn có thể tham khảo nhiều hàm thêm nửa tại
Result

Xử lý với những Errors khác module

Với những Errors đến từ các module khác( ở đây bao gồm cả errors từ std ), làm
thế nào chúng ta xử lý nó? Các đơn giản nhất là dùng những cách mình đã nói
trên, còn thêm một cách nửa là tự định nghĩa errors của chúng ta rồi tiến hành
convert những errors từ modules khác sang error của chúng ta :v, nghe thật vi
diệu, chỉ có errors thôi mà phải hành nhiều cách vãi. Nhưng khi đã quen với
Errors ở Rust bảo đảm bạn sẽ thích nó giống như mình.

Trước khi đi tiếp mình giới thiệu với bạn 1 tip: mọi người có nhớ try catch ở
lang khác thì ở rust có 1 macro try! và 1 operator question mark để try catch
:V

 let five = try!(parse("5".to_string()));
 // or
  let five = parse("5".to_string())?;

Mình tạm gọi 2 cái trên là operator để cho tiện, cả 2 operator trên đều thực
hiện pattern matching, riêng question mark thì nó là built-in lang. Đối với try!
nếu matching ok thì nó sẽ return lại value T đã được unwrap, còn có Err xảy ra
nó sẽ capture lại inner error và kế tiếp thực hiện convert sang error mình đã
định nghĩa thông qua trait From, và cuối cùng sẽ return về error mình đã định
nghĩa.

Lưu ý cả 2 operator trên chỉ dùng được trong những fn có return về là Result.

Giờ mình sẽ viết một hàm parse nhận input đầu vào là string, và kết quả trả về là giá trị đã được parse, nếu có errors thì trả về error.

Đối với những trường hợp này errors sẽ luôn xảy ra nên chúng ta không được bỏ qua bất kỳ trường hợp nào. Chúng ta không biết được input sẽ như nào nên tốt nhất là xử lý hết, trước hết sẽ đi từ đơn giản:

fn parse(input: String) -> Result<i32, String> {
    let temp = input.parse::<i32>();

    match temp {
        Ok(item) => Ok(item),
        Err(e) => Err("parse error".to_owned())
    }
}

fn main() {
    let five = parse("5".to_string());

    match five {
        Ok(item) => println!("parsed: {}", item),
        Err(e) => println!("error: {}", e)
    }
}

Như hàm trên thì chúng ta sẽ return Result<i32, String>, đây là kiểu custom của chúng ta, phần error sẽ trả về string parse error, tuy nhiên chúng ta có thể dùng description của error của std luôn: Err(e.to_string). Run chương trình với input là "5" thì output:

parsed: 5

Nếu chúng ta thay input bằng "invalid input", thì kết quả:

error: parse error

Hoặc nêu thay lại kiểu err:

 Err(e) => Err(e.to_string())

Thì kết quả đối với trường hợp invalid input:

error: invalid digit found in string

Nhưng thay vì mỗi hàm chúng ta phải thực hiện pattern như vậy ta có thể dùng try! hoặc ? để rút ngắn code lại, đống thời việc quản lý errors của chúng ta đơn giản hơn.

Nhưng 2 operators trên cần phải có 1 custom error của module chúng ta, để làm việc này chúng ta sẽ dùng type enum.

use std::num::ParseIntError;

#[derive(Debug)]
enum ParseError {
    IntError(ParseIntError),
}
fn parse(input: String) -> Result<i32, ParseError> {
    Ok(input.parse::<i32>()?)
}

Nếu nhìn lại đoạn code trong function parse nó đã được rút ngắn rất nhiều và hầu như là không rút ngắn được nửa. Với enum ParseError chúng ta thêm có thể có nhiều variant khác như như IntError, FloatError, hoặc JsonError

Lời ích của việc này là gì? Khi mà project to lên thì việc quản lý errors cực kỳ khó khăn với cách này chúng ta chỉ cần 1 module duy nhất và có thể reuse bất kỳ lúc nào :).

Tiếp tục với khai báo trên, nếu run thì sẽ báo một đống lỗi nhưng cơ bản có 2 lỗi chính sau:

  • Vì chúng ta dùng {} nên chúng ta phải implement trait fmt::Display
  • Phải implement trait convert::From bời vì 2 operators sẽ convert từ errors của function được call sang innert error của chúng ta khai báo

Full lỗi:

error[E0277]: the trait bound `ParseError: std::convert::From<std::num::ParseIntError>` is not satisfied
 --> <anon>:8:8
  |
8 |     Ok(input.parse::<i32>()?)
  |        ^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `ParseError`
  |
  = note: required by `std::convert::From::from`

error[E0277]: the trait bound `ParseError: std::fmt::Display` is not satisfied
  --> <anon>:16:41
   |
16 |         Err(e) => println!("error: {}", e)
   |                                         ^ the trait `std::fmt::Display` is not implemented for `ParseError`
   |
   = note: `ParseError` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
   = note: required by `std::fmt::Display::fmt`

Implement trait Display

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseError::IntError(ref e) => write!(f, "{}", e),
        }
    }
}

Implement trait convert::From

impl From<ParseIntError> for ParseError {
    fn from(err: ParseIntError) -> ParseError {
        ParseError::IntError(err)
    }
}

Cuối cùng run lại code

error: invalid digit found in string

Với code trên chúng ta dể dàng extend cài enum error kia

use std::num::ParseIntError;
use std::num::ParseFloatError;
use std::fmt;
use std::convert::From;

#[derive(Debug)]
enum ParseError {
    IntError(ParseIntError),
    FloatError(ParseFloatError),
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseError::IntError(ref e) => write!(f, "{}", e),
            ParseError::FloatError(ref e) => write!(f, "{}", e)
        }
    }
}

impl From<ParseIntError> for ParseError {
    fn from(err: ParseIntError) -> ParseError {
        ParseError::IntError(err)
    }
}

impl From<ParseFloatError> for ParseError {
    fn from(err: ParseFloatError) -> ParseError {
        ParseError::FloatError(err)
    }
}


fn parse(input: String) -> Result<f32, ParseError> {
    Ok(input.parse::<f32>()?)
}

fn main() {
    let five = parse("invalid input".to_string());

    match five {
        Ok(item) => println!("parsed: {}", item),
        Err(e) => println!("error: {}", e)
    }
}

Như đoạn code trên chúng ta chỉ cần thêm error parse float, mà không cần sửa gì về output error.

error: invalid float literal

Kết luận:

  • Dùng unwrap cho các trường hợp muốn test nhanh.
  • Dùng custom error khi cần quản lý nhiều errors khác nhau.
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Giang Nguyen

20 bài viết.
34 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
30 5
(Ảnh) Hai ngày nay mình đã tìm hiểu về Amazon S3, Cloudfront và Letsencrypt để xây dựng 2 trang web static, thứ nhất là trang chủ của (Link) và t...
Giang Nguyen viết hơn 1 năm trước
30 5
White
9 0
Đôi dòng về ID3 (Ảnh) Nếu bạn nào chưa biết thì có thể đọc phần này, hoặc biết rồi thì có thể next tới phần kế tiếp nhé. 1. Giới thiệu Như hì...
Giang Nguyen viết hơn 1 năm trước
9 0
White
7 0
Mặc định Phoenix dùng (Link) để thực hiện việc như transpile từ es6 javascription, hoặc compile từ scss/sass sang css. Tuy nhiên mình có đọc qua d...
Giang Nguyen viết 12 tháng trước
7 0
Bài viết liên quan
White
2 0
Adonis là một framework thường được gọi là Laravel của NodeJS. Khi tìm hiểu, bạn sẽ thấy Adonis rất giống Laravel, nhiều là đằng khác, cho nên việc...
ShinaBR2 viết 1 năm trước
2 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
20 bài viết.
34 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!