Giap Hiep

I'm Giap Hiep

I'm a web developer, a gymer. I enjoy share something i know that help people's work!
Giap Hiep

Toán tử điều hướng an toàn (&.) Trong Ruby

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 addressnil), hoặc trả về false nếu ownerfalse

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 ownerfalse 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

http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/