Nếu bạn đã theo dõi nỗ lực Ruby 3x3, chắc chắn bạn đã nghe nói về Optcarrot. Đó là một trình giả lập NES được viết bằng Ruby thuần túy.
Gần đây tôi đã xem qua nguồn Optcarrot và một chi tiết thú vị đã bị mắc kẹt với tôi. Nó sử dụng rộng rãi một tính năng của hàm băm của Ruby thường bị bỏ qua, nhưng khá hữu ích. Đó là khả năng sử dụng bất kỳ đối tượng nào làm khóa băm.
Bối cảnh:Bản đồ bộ nhớ NES
Là những lập trình viên cấp cao, chúng ta có xu hướng coi bộ nhớ là RAM. Nhưng ở cấp độ thấp hơn, "bộ nhớ" còn có nhiều công dụng khác.
Đọc và ghi vào "bộ nhớ" là cách CPU của NES giao tiếp với GPU, các tấm điều khiển và bất kỳ thiết bị điện tử đặc biệt nào trên hộp mực. Tùy thuộc vào địa chỉ được sử dụng, write_to_memory
lệnh gọi phương thức có thể đặt lại cần điều khiển, hoán đổi VRAM hoặc phát âm thanh.
Bạn sẽ triển khai điều này như thế nào trong Ruby?
Optcarrot thực hiện điều đó bằng cách lưu trữ hai Method
các đối tượng cho mỗi địa chỉ trong số 65536. Một là getter và một là setter. Nó trông giống như sau:
@getter_methods[0x0001] = @ram.method(:[])
@setter_methods[0x0001] = @ram.method(:[]=)
Sự cố:Đối tượng trùng lặp
Sự cố khi sử dụng phương thức Object#method
theo cách này là nó tạo ra nhiều Method
riêng lẻ các đối tượng giống hệt nhau.
Chúng ta có thể thấy điều này bằng cách xem object_id
:
> a = []
> a.method(:[]=).object_id
=> 70142391223600
> a.method(:[]=).object_id
=> 70142391912420
Hai Method
các đối tượng có object_id
khác nhau các giá trị, vì vậy chúng là các đối tượng khác nhau mặc dù chúng làm cùng một thứ.
Thông thường, chúng tôi có thể không quan tâm đến một vài Method
bổ sung nhưng trong trường hợp này, chúng tôi đang xử lý hàng nghìn đối tượng trong số chúng.
Giải pháp:Ghi nhớ thông qua một Hash
Optcarrot tránh trùng lặp Method
vấn đề đối tượng bằng một thủ thuật quá đơn giản nên dễ bị bỏ qua.
Nó sử dụng một hàm băm để ghi nhớ và loại bỏ trùng lặp. Đoạn mã đơn giản bên dưới thể hiện kỹ thuật:
def initialize
@setter_methods = []
@setter_cache = {}
...
end
def add_setter(address, setter)
# Doesn't store duplicates
@setter_cache[setter] ||= setter
# Use the deduped version
@setter_methods[address] = @setter_cache[setter]
end
Điều này hoạt động vì Hash
không quan tâm đến loại đối tượng nào bạn cung cấp cho nó làm chìa khóa.
Nếu điều này gây nhầm lẫn, hãy thử nó trong IRB với chuỗi:
> cache = {}
> cache["foo"] ||= "bar"
=> "bar"
cache["foo"] ||= "baz"
=> "bar"
Bây giờ, hãy xem xét rằng trong Ruby, các chuỗi là các thể hiện của lớp String
. Cơ chế Ruby sử dụng để sử dụng chuỗi làm khóa băm về cơ bản giống với cơ chế được sử dụng để lưu trữ Method
sự vật.
Cách Hash tính toán bình đẳng
Khi sử dụng các đối tượng không phải chuỗi làm khóa băm, câu hỏi đặt ra:Hash
như thế nào biết hai đối tượng có bằng nhau không?
Câu trả lời là nó sử dụng Object#hash
phương pháp. Phương thức này đi qua đối tượng của bạn và tạo một cách đệ quy một hàm băm. Nó trông như thế này:
> a.method(:[]=).hash
=> 929915641391564853
Bởi vì các đối tượng giống hệt nhau tạo ra các giá trị băm giống hệt nhau, nó có thể được sử dụng như một phép thử về tính bình đẳng.
a.hash == b.hash
Khá thú vị, đây là cách tiếp cận tương tự được sử dụng bởi eql?
phương pháp:
a.eql?(b)
Điều này hoạt động với Method
các đối tượng trong ví dụ của chúng tôi:
> a.method(:[]=).hash == a.method(:[]=).hash
=> true
Kết luận
Đã quen với các mẫu phát triển web của Ruby, tôi thực sự thú vị khi xem nguồn optcarrot và xem cách một ứng dụng không phải web trong thời gian thực sử dụng các mẫu khác nhau. Trong một ứng dụng web, tôi nghi ngờ mình sẽ tạo ra một mảng có 65536 phần tử, nhưng ở đây, là một phần của thiết lập cho một ứng dụng "máy tính để bàn", điều đó rất có ý nghĩa.
Nếu bạn có bất kỳ câu hỏi hoặc nhận xét nào, vui lòng liên hệ tại [email protected] hoặc @StarrHorne trên Twitter.