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ểuTthự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>
- 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!"), } - Sử dụng
if let: Cách ngắn gọn hơn khi bạn chỉ quan tâm đến trường hợpSome.Rustif let Some(s) = name { println!("Tên tìm được: {}", s); } else { println!("Không tìm thấy tên!"); } - 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 trongSome, hoặc trả vềdefault_valuenế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ểuT.Err(E): Thao tác thất bại và trả về một lỗi kiểuE.
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:
- Kiểm tra xem
Resultcó phải làErr(E)hay không. - Nếu là
Err(E), nó sẽ ngay lập tức trả về lỗi đó từ hàm hiện tại. - Nếu là
Ok(T), nó sẽ “bóc tách” giá trịTbê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 và ? 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:
