Tôi là Duyệt

Rust: Option & Result

Chuỗi bài viết Rust Tiếng Việt

Nhiều ngôn ngữ sử dụng kiểu dữ liệu null hoặc nil hoặc undefined để đại diện cho các giá trị rỗng hoặc không tồn tại, và sử dụng Exception để xử lý lỗi. Rust bỏ qua hai khái niệm này, để tránh gặp phải các lỗi phổ biến như null pointer exceptions, hay lộ thông tin nhạy cảm thông qua exceptions, … Thay vào đó, Rust giới thiệu hai generic enums OptionResult để giải quyết các vấn đề trên.

1. Option

Trong hầu hết các ngôn ngữ họ C (C, C#, Java, …), để xác định một cái gì đó failed hay không tìm được giá trị thỏa mãn, chúng ta thường trả về một giá trị “đặc biệt” nào đó. Ví dụ indexOf() của Javascript scan một phần tử trong mảng, trả về vị trí của phần tử đó trong mảng. Và trả về -1 nếu không tìm thấy.

Dẫn đến, ta sẽ thường thấy một số đoạn code như sau đây:

let sentence = "The fox jumps over the dog";
let index = sentence.indexOf("fox");

if (index > -1) {
  let result = sentence.substr(index);
  console.log(result);
}

Như bạn thấy -1 là một trường hợp đặc biệt cần xử lý. Có khi nào bạn đã từng mắc lỗi ngớ ngẫn vì tưởng giá trị đặc biệt đó là 0 chưa?

if (index > 0) {
  // 3000 days of debugging
}

"" hay null hay None cũng là một trong những trường hợp đặc biệt đó. Bạn đã từng nghe đến Null References: The Billion Dollar Mistake?

Lý do cơ bản là không có gì chắc chắn và có thể ngăn bạn lại việc … quên xử lý mọi trường hợp giá trị đặc biệt, hoặc do chương trình trả về các giá trị đặc biệt không như mong đợi. Có nghĩa là ta có thể vô tình làm crash chương trình với một lỗi nhỏ ở bất kỳ đâu, ở bất kỳ thời điểm nào.

Rust làm điều này tốt hơn, chỉ với Option.

Một giá trị optional có thể mang một giá trị nào đó Some(something) hoặc không mang giá trị nào cả (None).

// An output can have either Some value or no value/ None.
enum Option<T> { // T is a generic and it can contain any type of value.
  Some(T),
  None,
}

Theo thiết kế, mặc định bạn sẽ không bao giờ lấy được giá trị bạn cần nếu không xử lý các trường hợp có thể xảy ra với Option, là None chẳng hạn. Điều này được bắt buộc bởi compiler lúc compile code, có nghĩa là nếu bạn quên check, code sẽ không bao giờ được compile.

let sentence = "The fox jumps over the dog";
let index = sentence.find("fox");

if let Some(fox) = index {
  let words_after_fox = &sentence[fox..];
  println!("{}", words_after_fox);
}

(Rust Playground)

Cách sử dụng Option

Option là standard library, do đã được preludes nên chúng ta không cần khai báo trước khi sử dụng. Ngoài enum Option thì các variant của nó cũng đã được preludes sẵn như SomeNone.

Ví dụ, ta có một function tính giá trị chia hai số, đôi khi sẽ không tìm ra được kết quả, ta sử dụng Some nhu sau:

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
  if denominator == 0.0 {
    None
  } else {
    Some(numerator / denominator)
  }
}

fn main() {
  // The return value of the function is an option
  let result = divide(2.0, 3.0);

  // Pattern match to retrieve the value
  match result {
    // The division was valid
    Some(x) => println!("Result: {}", x),
    // The division was invalid
    None    => println!("Cannot divide by 0"),
  }
}

Ta thường sử dụng match để bắt giá trị trả về (Some hoặc None).

Bạn sẽ bắt gặp rất rất nhiều method khác nhau để xử lý giá trị của Option

Option method overview: https://doc.rust-lang.org/std/option/#method-overview

.unwrap()

Trả về giá trị nằm trong Some. Nếu giá trị là None có thể dẫn đến panic chương trình.

let x = Some("air");
assert_eq!(x.unwrap(), "air");

let x: Option<&str> = None;
assert_eq!(x.unwrap(), "air"); // panic!

.expect()

Giống .unwrap(), nhưng nếu panic thì Rust sẽ kèm theo message

let x: Option<&str> = None;
x.expect("fruits are healthy"); // panics: `fruits are healthy`

.unwrap_or()

Trả về giá trị nằm trong Some, nếu không trả về giá trị nằm trong or

assert_eq!(Some("car").unwrap_or("bike"), "car");

.unwrap_or_default()

Trả về giá trị nằm trong Some, nếu không trả về giá default.

let good_year_from_input = "1909";
let bad_year_from_input = "190blarg";
let good_year = good_year_from_input.parse().ok().unwrap_or_default();
let bad_year = bad_year_from_input.parse().ok().unwrap_or_default();

assert_eq!(1909, good_year);
assert_eq!(0, bad_year);

.ok_or()

Convert Option<T> sang Result<T, E>, mapping Some(v) thành Ok(v)None sang Err(err).

let x = Some("foo");
assert_eq!(x.ok_or(0), Ok("foo"));

match

Chúng ta có thể sử dụng pattern matching để code dễ đọc hơn

fn get_name(who: Option<String>) -> String {
  match who {
    Some(name) => format!("Hello {}", name),
    None       => "Who are you?".to_string(), 
  }
}

get_name(Some("duyet"));

2. Result

Tương tự như Option. Một kết quả trả về (Result) của một function thường sẽ có hai trường hợp:

  • thành công (Ok) và trả về kết quả
  • hoặc lỗi (Err) và trả về thông tin lỗi.
enum Result<T, E> {
  Ok(T),
  Err(E),
}

Ví dụ

fn get_age(who: &str) -> Result<i8, &str> {
  if who == "duyet" {
    Ok(18)
  } else {
    Err("unknown")
  }
}

fn main() -> Result<(), &'static str> {
  let who = "duyet";

  match get_age(who) {
    Ok(age)  => println!("{} is {}", who, age),
    Err(err) => println!("Err: {}", err),
  }

  Ok(())
}

(Rust Playground)

Như bạn thấy thì main() cũng có thể return về Result<(), &'static str>

.unwrap()

Ví dụ trên nhưng sử dụng .unwrap() , chủ động panic (crash) dừng chương trình nếu gặp lỗi.

fn main() -> Result<(), &'static str> {
  let who = "duyet";
  let age = get_age(who).unwrap();
  println!("{} is {}", who, age);

  Ok(())
}

(Rust Playground)

.expect()

Giống như unwrap(): chủ động panic (crash) dừng chương trình nếu gặp lỗi và kèm theo message. Sẽ rất có ích, nhất là khi có quá nhiều unwrap, bạn sẽ không biết nó panic ở đâu.

fn main() -> Result<(), &'static str> {
  let who = "ngan";
  let age = get_age(who).expect("could not get age");
  println!("{} is {}", who, age);

  Ok(())
}

(Rust Playground)

Xem thêm mọi method khác của Result tại đây.

3. Convert Result sang Option

Đôi khi bạn sẽ cần convert từ Ok(v) sang Some(v) hoặc Err(e) sang Some(e)

// .ok(v) = Some(v)
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

// .err()
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

4. Toán tử ?

Khi viết code mà có quá nhiều functions trả về Result, việc handle Err sẽ khá nhàm chán. Toán tử chấm hỏi ? cho phép dừng function tại vị trí đó và return cho function cha nếu Result ở vị trí đó là Err.

Nó sẽ thay thế đoạn code sau:

use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  // Early return on error
  let mut file = match File::create("my_best_friends.txt") {
    Err(e) => return Err(e),
    Ok(f) => f,
  };
  if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
    return Err(e)
  }
  Ok(())
}

thành

use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  let mut file = File::create("my_best_friends.txt")?;
  // Early return on error
  file.write_all(format!("name: {}\n", info.name).as_bytes())?;
  file.write_all(format!("age: {}\n", info.age).as_bytes())?;
  file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
  Ok(())
}

Gọn đẹp hơn rất nhiều.

Toán tử ? sẽ unwrap giá trị Ok, hoặc return giá trị Err ở vị trí gần toán tử đó.

? chỉ có thể được dùng trong function có kiểu dữ liệu trả về là Result.