Toán tử điều hướng an toàn (&.) Trong Ruby
Bài đăng này đã không được cập nhật trong 4 năm
Kể từ phiên bản 2.3.0 trở đi, ngôn ngữ Ruby dược bổ sung thêm một toán tử điều hướng (&.) vô cùng thú vị. Toán tử này đã có mặt trong C # và Groovy trong một thời gian dài với cú pháp hơi khác nhau -? .. Vậy nó làm gì?
Tình huống
Ví dụ trong cơ sowe dữ liệu chúng ta có một bảng Account
và một bảng Owner
có quan hệ 1- 1 với nhau. Giả sử ta có một đối tượng account
và muốn lấy ra thông tin address
của account đó thống qua owner
để không gặp lỗi thì ta thường viết như sau:
if account && account.owner && account.owner.address
...
end
Cách này thực sự rất dài dòng và gây khó hiểu cho người đọc. Đó cũng chính là một trong số nguyên nhân mà ActiveSupport
được bao gồm phương thức try
, ta có thể viết lại đoạn code cho ngắn gọn hơn như sau:
if account.try(:owner).try(:address)
...
end
Hai cách này hoạt động tương tự nhau. Điều kiện trên sẽ trả về address
nếu tồn tại address
trong owner
(trả về nil
nếu address
là nil
), hoặc trả về false
nếu owner
là false
Sử dụng toán tử điều hướng an toàn (&.)
Chúng ta có thể viết lại ví dụ ở trên bằng toán tử điều hướng an toàn như sau:
if account&.owner&.address
...
end
Nếu chưa có hai ví dụ ở trên thì có vẻ cú pháp có vẻ hơi khó hiểu tuy nhiên trông đoạn code gọn hơn rất nhiều.
Ví dụ
Trong ví dụ này tôi sẽ so sánh cả ba cách ở trên với nhau:
account = Account.new(owner: nil) # account without an owner
account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass
account && account.owner && account.owner.address
# => nil
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => nil
Với cách giải thích phía trên có vẻ như đầu ra chúng ta có thể lường trước được. Nhưng nếu owner
là false
thì sao?
account = Account.new(owner: false)
account.owner.address
# => NoMethodError: undefined method `address' for false:FalseClass `
account && account.owner && account.owner.address
# => false
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => undefined method `address' for false:FalseClass`
Điều bất ngờ đầu tiên là cú pháp &.
chỉ bỏ qua nil
nhưng lại nhận ra false
,
nó không chính xác tương đương với cú pháp s1 && s1.s2 && s1.s2.s3
Điều gì sảy ra nếu tồn tại owner
nhưng lại không có address
account = Account.new(owner: Object.new)
account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>
account && account.owner && account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
account.try(:owner).try(:address)
# => nil
account&.owner&.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
Rất tiếc là phương thưc try
không kiểm tra nếu đối tượng phản hồi với phương thức đã cho.
Để nghiêm ngặt hơn chúng ta sử dụng phương thức try!
account.try!(:owner).try!(:address)
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`
Rủi ro
Hãy cẩn thận khi sử dụng toán tử & .
và kiểm tra các giá trịnil
. Hãy xem xét ví dụ sau:
nil.nil?
# => true
nil?.nil?
# => false
nil&.nil?
# => nil
Ở ví dụ cuối cùng khá là khó hiểu vì nil&.nil?
nên trả về true
. Tuy nhiên đó là một lưu ý
Array#dig và Hash#dig
Một phương thức được bổ sung thêm nữa là #dig
, thay vì phải viết một đoạn code như thế này:
address = params[:account].try(:[], :owner).try(:[], :address)
# or
address = params[:account].fetch(:owner) { {} }.fetch(:address)
Chúng ta có thể xử lý đơn giản hơn với Hash#dig
address = params.dig(:account, :owner, :address)
Lời kết
Đối với một ngôn ngữ động thì việc xử lý những giá trị nil
là khá rắc rối, vì thế việc bổ sung các toán tử điều hướng an toàn và phương thức dig
là rất cấn thiết.
Tham khảo
All rights reserved