Trong khoảng sáu tháng qua, tôi đã làm việc với trình giả lập NES trong Rust. Như bạn có thể mong đợi, tôi đã học được rất nhiều về rỉ sét và thậm chí nhiều hơn nữa về các bộ phận bên trong NES. Nhưng trải nghiệm cũng đã thay đổi cách tôi nhìn nhận về Ruby.
Cụ thể, nó khiến tôi hơi hoang tưởng về các phương thức trả về nil
.
Nếu bạn không ủng hộ điều gì đó, bạn sẽ gục ngã vì bất cứ điều gì
Cái gì nil
nghĩa là trong Ruby? Gần như mọi thứ. Khi một phương thức trả về nil, nó có thể có nghĩa là:
- Phương thức không có giá trị trả về
- Thường có một giá trị trả lại nhưng không phải lúc này
- Nó trả về một giá trị từ cơ sở dữ liệu, là NULL
- Có điều gì đó không mong muốn đã xảy ra
Điều này làm cho mã khó đọc và là nguyên nhân chính của phổ biến nhất Ngoại lệ Ruby 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 đưa con tôi đi học.
Nhìn vào đoạn mã sau. Nó trả về nil
hầu hết thời gian vì if
các câu lệnh đá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 có kinh nghiệm, bạn biết cái lỗ thỏ này còn sâu hơn nhiều. Đôi khi những ý nghĩa khác nhau này của nil trùng lặp theo những cách kỳ lạ, khiến chúng ta không thể biết liệu - ví dụ - một giá trị trong cơ sở dữ liệu có phải là NULL
hay không hoặc không có giá trị nào trong cơ sở dữ liệu.
Một cách tốt hơn
Trong Rust không có thứ gọi là nil
. Thay vào đó, khi chúng ta muốn biểu thị rằng một hàm đôi khi trả về một giá trị và đôi khi trả về "không có gì", chúng ta sử dụng Option
.
Options
là một kiểu chứa một số giá trị cụ thể hoặc không chứa giá trị nào. Đây là những gì chúng trông giống như trong mã:
Option::Some(42); // Wraps the number 42 in an option
Option::None; // Indicates "no result"
Điều này đã trông đẹp hơn so với nil
đặc biệt của chúng tôi sử dụng, nhưng nó thậm chí còn tốt hơn. Trình biên dịch Rust buộc bạn phải xem xét None
trường hợp. Bạn không thể vô tình bỏ qua nó.
match my_option {
Some(x) => do_something_with_x(x),
// If you remove the `None` match below, this code
// won't compile.
None => do_the_default_thing()
}
Vì vậy, chúng tôi có thể viết ví dụ đặt tên màu của chúng tôi bằng gỉ như sau:
fn color_name(rgb: u32) -> Option<String> {
if rgb == 0x000000 {
Some("black".to_owned())
} else {
None
}
}
Bây giờ chúng tôi buộc phải xử lý cả Some
và None
điều kiện:
let name = color_name(0xFFFFFF);
let name = match color_name(0xFFFFFF) {
Some(value) => value,
None => "unknown".to_owned(),
}
Chắc chắn điều này hơi dài dòng và trông kỳ lạ, nhưng nó khiến bạn không thể bỏ qua trường hợp một hàm không trả về giá trị hữu ích. Điều đó có nghĩa là mã dễ hiểu và dễ bảo trì hơn.
Triển khai tùy chọn trong Ruby
Điều mà Ruby thiếu ở tính nghiêm ngặt, nó bù đắp cho sự linh hoạt. Tôi nghĩ sẽ rất thú vị nếu cố gắng triển khai một cái gì đó như Option
trong Ruby.
Chúng tôi không thể tạo lỗi thời gian biên dịch trong Ruby, vì nó là một ngôn ngữ thông dịch. Nhưng chúng tôi có thể khiến mã không chính xác luôn luôn nêu ra một ngoại lệ, thay vì chỉ nâng một ngoại lệ khi bạn gặp một trường hợp cạnh.
Đầu tiên, hãy tạo hai lớp. Some
giữ một giá trị chỉ đọc. None
trống rỗng. Chúng đơn giản như chúng có vẻ.
class Some
attr_reader :value
def initialize(value)
@value = value
end
end
class None
end
Tiếp theo, chúng tôi sẽ tạo Option
lớp chứa Some
hoặc None
và chỉ cho phép chúng tôi truy cập chúng khi chúng tôi 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 "Option value must be either Some or None"
end
end
end
Cuối cùng, chúng ta có thể viết lại ví dụ về màu sắc của mình để sử dụng Option
mới lớp:
def color_name(rgb)
if rgb == 0x000000
Option.some("black")
else
Option.none()
end
end
puts color_name(0x000000).match(
-> value { value },
-> { "no match" })
# Prints "black"
Kết luận
Tôi vẫn chưa thử kỹ thuật này trong một dự án thực tế. Tôi nghĩ rằng nó chắc chắn có thể ngăn chặn rất nhiều NoMethodErrors
luôn luôn được đưa vào sản xuất. Nó trông hơi rườm rà và không giống Rubyish lắm 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.