Option

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.


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:

// Typescript

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?

// Typescript

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).

#![allow(unused)]
fn main() {
// 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.

#![allow(unused)]
fn main() {
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);
}
}

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 như sau:

fn get_id_from_name(name: &str) -> Option<i32> {
    if !name.starts_with('d') {
        return None;
    }

    Some(123)
}

fn main() {
    let name = "duyet";

    match get_id_from_name(name) {
        Some(id) => println!("User = {}", id),
        _ => println!("Not found"),
    }
}

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 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(T). Nếu giá trị là None thì panic chương trình.

#![allow(unused)]
fn main() {
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 khi panic thì Rust sẽ kèm theo message

#![allow(unused)]
fn main() {
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

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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).

#![allow(unused)]
fn main() {
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

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

get_name(Some("duyet"));
}

if let Some(x) = x

Có thể bạn sẽ gặp pattern này nhiều khi đọc code Rust. Nếu giá trị của xSome thì sẽ destruct giá trị đó bỏ vào biến x nằm trong scope của if.

#![allow(unused)]
fn main() {
fn get_data() -> Option<String> {
    Some("ok".to_string())
}

if let Some(data) = get_data() {
    println!("data = {}", data);
} else {
    println!("no data");
}
}