Sáu tháng qua, tôi đã đang xây dựng một trình giả lập NES bằng Rust. Như bạn có thể dự đoán, tôi đã học được rất nhiều về Rust, và thậm chí nhiều hơn về cấu trúc bên trong của NES. Nhưng trải nghiệm này cũng đã thay đổi cách tôi nhìn nhận Ruby.
Cụ thể, nó khiến tôi trở nên cực kỳ lo lắng về các phương thức trả về nil.
Nếu Bạn Không Vững Vàng Về Điều Gì, Bạn Sẽ Bị Lừa Bởi Tất Cả
nil trong Ruby có nghĩa là gì? Gần như bất cứ điều gì. Khi một phương thức trả về nil, nó có thể意味着:
- Phương thức không có giá trị trả về
- Thường có giá trị trả về nhưng lần này không có
- Nó trả về một giá trị từ cơ sở dữ liệu, là NULL
- Đã xảy ra điều gì đó bất ngờ
Điều này khiến code khó đọc và là nguyên nhân chính của ngoại lệ phổ biến nhất trong Ruby: NoMethodError. Là chủ sở hữu một phần của dịch vụ giám sát ngoại lệ, NoMethodError đang giúp tôi nuôi con đi học.
Xem đoạn code sau. Nó trả về nil hầu hết thời gian vì các câu lệnh if đánh giá thành nil khi điều kiện không khớp và không có else.
def color_name(rgb)
if rgb == 0x000000
"black"
end
end
color_name("#FFFFFF").titleize
# => NoMethodError: undefined method `titleize' for nil:NilClass
Nếu bạn là một nhà phát triển Ruby giàu kinh nghiệm, bạn biết hố thỏ này sâu hơn nhiều. Đôi khi các ý nghĩa khác nhau của nil chồng chéo lên nhau theo những cách kỳ lạ, khiến không thể biết được liệu — ví dụ — một giá trị trong cơ sở dữ liệu có phải là NULL, hay根本 không có giá trị nào trong cơ sở dữ liệu cả.
Một Cách Tốt Hơn
Trong Rust không có khái niệm nil. Thay vào đó, khi chúng ta muốn biểu thị một hàm đôi khi trả về giá trị và đôi khi trả về "không có gì", chúng ta sử dụng Option.
Option là một kiểu dữ liệu hoặc chứa một giá trị cụ thể, hoặc không chứa giá trị nào. Đây là cách chúng trông trong code:
Option::Some(42); // Gói số 42 trong một option
Option::None; // Chỉ thị "không có kết quả"
Điều này đã trông tốt hơn việc sử dụng nil tùy hứng, nhưng nó còn tốt hơn nữa. Trình biên dịch Rust buộc bạn phải xem xét trường hợp None. Bạn không thể vô tình bỏ qua nó.
match my_option {
Some(x) => do_something_with_x(x),
// Nếu bạn xóa match `None` bên dưới, code
// sẽ không biên dịch được.
None => do_the_default_thing()
}
Vì vậy, chúng ta có thể viết ví dụ đặt tên màu sắc trong Rust như sau:
fn color_name(rgb: u32) -> Option<String> {
if rgb == 0x000000 {
Some("black".to_owned())
} else {
None
}
}
Bây giờ chúng ta bị buộc phải xử lý cả hai điều kiện Some và None:
let name = color_name(0xFFFFFF);
let name = match color_name(0xFFFFFF) {
Some(value) => value,
None => "unknown".to_owned(),
}
Chắc chắn đây hơi fl dài và trông kỳ lạ, nhưng nó làm cho việc bỏ qua trường hợp hàm không trả về giá trị hữu ích trở nên không thể. Điều đó có nghĩa là code dễ hiểu và dễ bảo trì hơn.
Triển Khai Option Trong Ruby
Ruby thiếu sự chặt chẽ nhưng bù lại bằng sự linh hoạt. Tôi nghĩ sẽ thú vị nếu thử triển khai cái gì đó giống Option trong Ruby.
Chúng ta không thể tạo ra lỗi thời gian biên dịch trong Ruby, vì nó là ngôn ngữ thông dịch. Nhưng chúng ta có thể khiến code không chính xác luôn luôn ném ngoại lệ, thay vì chỉ ném ngoại lệ khi gặp trường hợp cực đoan.
Đầu tiên, hãy tạo hai lớp. Some giữ một giá trị chỉ đọc. None là rỗng. Chúng đơn giản như chúng nghe.
class Some
attr_reader :value
def initialize(value)
@value = value
end
end
class None
end
Tiếp theo, chúng ta sẽ tạo lớp Option của mình, giữ hoặc Some hoặc None và chỉ cho phép truy cập chúng khi chúng ta cung cấp trình xử lý cho cả hai.
class Option
def initialize(value)
@value = value
end
def self.some(value)
self.new(Some.new(value))
end
def self.none()
self.new(None.new)
end
def match(some_lambda, none_lambda)
if @value.is_a?(Some)
some_lambda.call(@value.value)
elsif @value.is_a?(None)
none_lambda.call()
else
raise "Giá trị Option phải là Some hoặc None"
end
end
end
Cuối cùng, chúng ta có thể viết lại ví dụ màu sắc để sử dụng lớp Option mới:
def color_name(rgb)
if rgb == 0x000000
Option.some("black")
else
Option.none()
end
end
puts color_name(0x000000).match(
-> value { value },
-> { "no match" }
) # In ra "black"
Kết Luận
Tôi chưa thử kỹ thuật này trong một dự án thực tế. Tôi nghĩ nó chắc chắn có thể ngăn chặn nhiều NoMethodErrors luôn lẻn vào production. Nó hơi cồng kềnh và không rất "Rubyish", nhưng tôi tưởng tượng rằng với một số cải tiến, một cú pháp dễ chịu hơn sẽ xuất hiện.