Công việc mà toán tử ||= của Ruby thực sự thực hiện

Trong lúc làm việc tôi có động đến việc cache dữ liệu bằng biến instance thông qua toán tử ||=, thấy hay hay nên quyết định search thử xem toán tử này hoạt động ra sao thì tìm được một bài viết, tôi quyết định dịch lại cho mọi người tham khảo. Link bài viết gốc ở đây

Tổng quát

Có một sự nhầm lẫn thông thường là a ||= b tương đương với a = a || b, những toán tử đó lại hoạt động như a || a = b

Ở phép gán a = a || b, a được gán bằng một thứ gì đó mỗi lần phép gán được chạy, trong khi với a || a = b, a chỉ được gán nếu anil hoặc false. Do đó nếu vế trái của || là true thì không cần phải xét đến vế phải.

Sự hiểu nhầm thông thường

a ||= b tương đương với a = a || b là một cách giải thích phổ biến bởi hai lý do:

  • nếu ab đều là biến cục bộ, a = a || b là một cách biểu diễn ngắn gọn và tự nhiên.
  • các toán tử khác như +=-= hoạt động theo cách này, ví dụ a += b tương đương với a = a + b

Chuyện gì sẽ xảy ra nếu không phải a = a || b?

##Ví dụ ban đầu Dưới đây là một ví dụ sử dụng đơn giản của a ||= b:


a = nil
b = 20
a ||= b
a        # => 20

##Ví dụ đầy đủ đối với Hash và Array Hãy thử một vài ví dụ phức tạp hơn:


h = {}

def h.[]=(k, v)
  puts "Setting hash key #{k} with #{v.inspect}"
  super
end

# 1. Sử dụng ||=

h[:x] ||= 10
h[:x] ||= 20

# 2. Phép a = a || b

h[:y] = h[:y] || 10
h[:y] = h[:y] || 20

# 3. Phép a || a = b

h[:z] || h[:z] = 10
h[:z] || h[:z] = 20

Kết quả


Setting hash key x with 10
Setting hash key y with 10
Setting hash key y with 10
Setting hash key z with 10

Ở trường hợp đầu, sử dụng ||=, key của hash chỉ được gán giá trị một lần. Một khi gía trị khác nilfalse, h[:x] không được gán gía trị mới nữa.

Ở trường hợp thứ 2, sử dụng phép a = a || b có xảy ra hai phép gán. Gía trị vẫn là 10 nhưng h[:y] được gán gía trị của chính nó như là gía trị mới.

Ở trường hợp cuối, cách hành xử tương tự như trường hợp đầu cho thấy phép a || a = b gần với phép ||= hơn.

##Ví dụ đầy đủ với các method getter/setter Một kết qủa tương tự cũng xảy ra nếu chúng ta xem xét đến các method getter/setter của object


class MyClass
  attr_reader :val

  def val=(val)
    puts "Setting val to #{val.inspect}"
    @val = val
  end
end

# 1. Sử dụng ||=

obj = MyClass.new
obj.val ||= 'a'
obj.val ||= 'b'

# 2. Phép a = a || b

obj = MyClass.new
obj.val = obj.val || 'c'
obj.val = obj.val || 'd'

# 3. Phép a || a = b

obj = MyClass.new
obj.val || obj.val = 'e'
obj.val || obj.val = 'f'

Và kết qủa tương tự với ví dụ của hash và array:


Setting val to "a"
Setting val to "c"
Setting val to "c"
Setting val to "e"

##Gía trị mặc định của Hash: một trường hợp kỳ lạ? Trở lại năm 2008, David Black nhận thấy một trường hợp kỳ lạ với hash có gía trị mặc định. Nếu bạn bám theo logic ở phía trên thì trường hợp này sẽ không làm bạn ngạc nhiên, dù từ góc nhìn thực hành thì nó khá gây tò mò.


hsh = Hash.new('default')

hsh[:x]         # => 'default'

# 1. Sử dụng ||=

hsh[:x] ||= 10
p hsh           # => {}

# 2. Phép a = a || b

hsh[:y] = hsh[:y] || 10
p hsh           # {:y=>"default"}

# 3. Phép a || a = b

hsh[:z] || hsh[:z] = 10
p hsh           # {:y=>"default"}

Hash với gía trị mặc định sẽ hoạt động theo một cách thú vị, dựa vào cách bạn nhìn nhận. Chỉ truy cập gía trị không có nghĩa là gía trị đó đã được lưu giữ lại trong hash đó. Lý do là bạn có thể truyền Proc vào default_proc của hash để thực hiện tính toán (hay thậm chí là gán gía trị) khi một key chưa được set được truy cập.

Một lần nữa chúng ta chú ý rằng cách viết a || a = b cho kết qủa gần với ||= nhất.

##Biến chưa được định nghĩa: một trường hợp kỳ lạ khác Trong phần bình luận của bài viết, Vikrant Chaudhary đã cho thấy một trường hợp thú vị khác:


Nếu a chưa được định nghĩa thì a || a = 42 sẽ sinh ngoại lệ NameError, trong khi a ||= 42 trả về 42. Vậy có vẻ chúng không tương đương với nhau.

May mắn là tôi đã nói chúng "hành xử" tương đương nhau - phew. Bỏ qua việc đùa vui, Vikrant Chaudhary đã cho thấy một điểm thú vị.

Trường hợp này giống một chút với trường hợp hash. Các mà Ruby thực hiện tính toán đã sinh ra trường hợp như vậy. Cụ thể là, phép gán, dù không chạy, cũng sẽ sinh ra biến. Ví dụ:


x = 10 if 2 == 5
puts x

Dù dòng đầu tiên không chạy nhưng x vẫn tồn tại ở dòng thứ 2 và không có ngoại lệ nào sinh ra. Một trường hợp khác:


x = x
puts x

Chà, a ||= 42 cũng hoạt động tương tự vậy. Ruby thấy phép gán ở bước phân tích và tạo ra biến trong khi a || a = 42 thì không, dù nó hành xử tương tự với a || a = 42 một khi được thực thi.