🦀 Rust Cơ Bản – Bài 4: Xử lý lỗi trong Rust: Result, Option và toán tử ?

5 min read

Xử lý lỗi là một khía cạnh cốt lõi của việc lập trình an toàn, và Rust giải quyết vấn đề này bằng một cách tiếp cận độc đáo, ưu tiên sự rõ ràng và tránh xa các ngoại lệ (exceptions) như trong nhiều ngôn ngữ khác.

Trong bài viết này, chúng ta sẽ khám phá hai kiểu liệt kê (enum) quan trọng nhất để xử lý các giá trị có thể thiếu hoặc các lỗi có thể xảy ra: Option<T>, Result<T, E>, và cách toán tử ? giúp việc viết mã trở nên sạch sẽ hơn.

1. Option<T>: Xử Lý Giá Trị Có Thể Thiếu

Option<T> là một kiểu liệt kê dùng để mã hóa khái niệm một giá trị có thể có hoặc có thể không có. Nó giải quyết vấn đề của các tham chiếu null (như null trong Java/C# hoặc NULL trong C/C++) một cách an toàn, buộc lập trình viên phải xử lý trường hợp không có giá trị.

pub enum Option<T> {
 None, Some(T)
}
  • Some(T): Một giá trị kiểu T thực sự tồn tại.
  • None: Không có giá trị.

Ví dụ: Tìm phần tử trong Vec

Khi bạn cố gắng truy cập một phần tử của Vec (vector) thông qua phương thức pop(), nó sẽ trả về Option<T>:

Rust

let mut numbers = vec![1, 2, 3];

// Trả về Some(3)
let last_element = numbers.pop(); 
println!("{:?}", last_element); 

// Trả về None
let last_element = numbers.pop().expect("Cần xử lý trường hợp này!"); 
println!("{:?}", last_element); 
// ... sau khi pop 3 lần, numbers.pop() sẽ trả về None

Cách Xử Lý Option<T>

  1. Sử dụng match: Đây là cách xử lý rõ ràng và an toàn nhất, buộc bạn phải xem xét cả hai trường hợp.Rustlet name = get_name_from_db("404"); // Giả sử hàm này trả về Option<String> match name { Some(s) => println!("Tên tìm được: {}", s), None => println!("Không tìm thấy tên!"), }
  2. Sử dụng if let: Cách ngắn gọn hơn khi bạn chỉ quan tâm đến trường hợp Some.Rustif let Some(s) = name { println!("Tên tìm được: {}", s); } else { println!("Không tìm thấy tên!"); }
  3. Các phương thức tiện ích (unwrap_or, map, ok_or, v.v.):
    • unwrap_or(default_value): Trả về giá trị bên trong Some, hoặc trả về default_value nếu là None.
    • unwrap() / expect("message"): KHÔNG NÊN DÙNG trong mã nguồn thật, vì chúng sẽ panic! (thoát chương trình) nếu giá trị là None.

2. Result<T, E>: Xử Lý Lỗi Có Thể Khắc Phục

Result<T, E> là lựa chọn hàng đầu của Rust để xử lý các lỗi có thể xảy ra (recoverable errors), chẳng hạn như lỗi I/O (Input/Output). Giống như Option<T>, nó là một kiểu liệt kê:

pub enum Result<T, E> {
 Ok(T), Err(E)
}
  • Ok(T): Thao tác thành công và trả về một giá trị kiểu T.
  • Err(E): Thao tác thất bại và trả về một lỗi kiểu E.

Ví dụ: Đọc file

Hàm std::fs::read_to_string trả về Result<String, std::io::Error>, vì việc đọc file có thể thất bại (ví dụ: file không tồn tại, không có quyền truy cập, v.v.).

Rust

use std::fs::File;

fn open_config_file() -> Result<String, std::io::Error> {
    // File::open trả về Result<File, std::io::Error>
    File::open("config.txt")?; 
    
    // Giả sử có thêm bước khác
    Ok(String::from("Nội dung đã đọc"))
}

Cách Xử Lý Result<T, E>

Bạn xử lý Result tương tự như Option, bằng cách sử dụng match hoặc if let để bóc tách giá trị Ok hoặc lỗi Err.

Rust

let result = std::fs::read_to_string("hello.txt");

match result {
    Ok(content) => println!("Nội dung file: {}", content),
    Err(error) => {
        // Xử lý các loại lỗi khác nhau
        eprintln!("Đã xảy ra lỗi khi đọc file: {:?}", error);
    }
}

3. Toán Tử ? (The Question Mark Operator)

Khi bạn viết mã cần gọi nhiều hàm trả về Result, việc sử dụng match lồng nhau có thể làm code trở nên cồng kềnh. Toán tử ? được sinh ra để giải quyết vấn đề này.

Cách Hoạt Động

Toán tử ? là cách viết tắt cho:

  1. Kiểm tra xem Result có phải là Err(E) hay không.
  2. Nếu là Err(E), nó sẽ ngay lập tức trả về lỗi đó từ hàm hiện tại.
  3. Nếu là Ok(T), nó sẽ “bóc tách” giá trị T bên trong và tiếp tục thực thi.

Yêu cầu quan trọng: Hàm chứa toán tử ? phải có kiểu trả về là Result.

Ví dụ So Sánh

Không dùng ?:

Rust

fn read_username_from_file_manual() -> Result<String, std::io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e), // Trả về lỗi nếu không mở được
    };
    
    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e), // Trả về lỗi nếu không đọc được
    }
}

Dùng toán tử ?:

Rust

use std::io::{self, Read};

fn read_username_from_file_shorthand() -> Result<String, io::Error> {
    // Nếu File::open trả về Err, nó sẽ trả về Err từ hàm này.
    // Nếu là Ok, nó gán giá trị File cho 'username_file'.
    let mut username_file = File::open("hello.txt")?; 

    let mut username = String::new();
    
    // Nếu read_to_string trả về Err, nó sẽ trả về Err từ hàm này.
    // Nếu là Ok, nó tiếp tục.
    username_file.read_to_string(&mut username)?; 

    Ok(username)
}

Bạn có thể thấy, toán tử ? giúp code sạch sẽ và dễ đọc hơn rất nhiều, tập trung vào happy path của chương trình.

4. Tổng kết

Thay vì dựa vào cơ chế try...catch ẩn (exceptions), Rust đặt việc xử lý mọi kết quả có thể xảy ra ngay trước mắt bạn thông qua hai kiểu dữ liệu tường minh:

  • Option: Đảm bảo bạn không bao giờ phải đối mặt với nỗi sợ hãi mang tên Null.
  • Result: Cung cấp một giao diện rõ ràng để phân tách kết quả thành công khỏi nguyên nhân thất bại.

Và khi bạn làm việc với chuỗi các thao tác có thể thất bại, Toán tử ? giúp code của bạn vừa an toàn, tập trung vào mục đích chính.

Việc nắm vững Option, Result? không chỉ là “học Rust” mà là học cách xây dựng phần mềm đáng tin cậy. Chúng là lý do tại sao Rust liên tục được bình chọn là ngôn ngữ lập trình được yêu thích nhất.

Phần 3: https://ant.ncc.plus/?p=20403

Reference:

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *