Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tận Dụng Object Làm Khóa Hash Trong Ruby: Một Tính Năng Mạnh Mẽ Bị Bỏ Quên

Nếu bạn đã theo dõi nỗ lực Ruby 3x3, bạn có thể đã nghe về Optcarrot. Đó là một trình giả lập NES viết thuần bằng Ruby.

Tôi gần đây đang xem qua source Optcarrot và một chi tiết thú vị nổi bật. Nó tận dụng nhiều một tính năng của hash Ruby thường bị bỏ qua, nhưng khá hữu ích. Đó là khả năng dùng bất kỳ object nào làm khóa hash.

Ngữ cảnh: Ánh xạ bộ nhớ NES

Là lập trình viên high-level, chúng ta tend to nghĩ về bộ nhớ như RAM. Nhưng ở lower-level, "bộ nhớ" có nhiều dùng khác.

Đọc và ghi vào "bộ nhớ" là cách CPU của NES giao tiếp với GPU, control pads và bất kỳ điện tử đặc biệt nào trên cartridge. Tùy vào địa chỉ dùng, một lời gọi phương thức write_to_memory có thể reset joystick, swap VRAM hoặc phát âm thanh.

Làm sao bạn triển khai điều này trong Ruby?

Optcarrot làm nó bằng cách lưu hai đối tượng Method cho mỗi trong 65536 địa chỉ. 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(:[]=)

Vấn đề: Đối tượng trùng lặp

Vấn đề với việc dùng Object#method theo cách này là nó tạo ra rất nhiều đối tượng Method riêng lẻ mà giống hệt nhau.

Chúng ta có thể thấy điều này bằng cách nhìn object_id:

> a = []
> a.method(:[]=).object_id
=> 70142391223600
> a.method(:[]=).object_id
=> 70142391912420

Hai đối tượng Method có giá trị object_id khác nhau, vì vậy chúng là đối tượng khác nhau mặc dù làm cùng một việc.

Bình thường, chúng ta có thể không quan tâm vài đối tượng Method thêm, nhưng trong trường hợp này chúng ta đang deal với hàng nghìn cái.

Giải pháp: Memoize qua Hash

Optcarrot tránh vấn đề đối tượng Method trùng lặp với một thủ thuật đơn giản đến mức dễ bỏ qua.

Nó dùng một hash để memoize và deduplicate. Mã đơn giản hóa dưới đây chứng minh kỹ thuật:

def initialize
  @setter_methods = []
  @setter_cache = {}
  ...
end
def add_setter(address, setter)
  # Không lưu trùng lặp
  @setter_cache[setter] ||= setter
  # Dùng phiên bản deduped
  @setter_methods[address] = @setter_cache[setter]
end

Điều này hoạt động vì Hash không quan tâm bạn cho nó loại object nào làm khóa.

Nếu điều này gây nhầm lẫn, hãy thử trong IRB với strings:

> cache = {}
> cache["foo"] ||= "bar"
=> "bar"
cache["foo"] ||= "baz"
=> "bar"

Bây giờ, xem xét rằng trong Ruby, strings là instance của class String. Cơ chế Ruby dùng để dùng string làm khóa hash cơ bản giống với cái dùng để lưu đối tượng Method.

Hash tính toán bằng nhau như thế nào

Khi dùng object non-string làm khóa hash, câu hỏi nảy ra: Hash biết làm sao hai object bằng nhau?

Câu trả lời là nó dùng phương thức Object#hash. Phương thức này đi qua object của bạn và đệ quy generate một hash. Nó trông như sau:

> a.method(:[]=).hash
=> 929915641391564853

Vì object giống nhau tạo ra giá trị hash giống nhau, nó có thể dùng như test bằng nhau.

a.hash == b.hash

Thú vị, đây là cách tiếp cận giống như phương thức eql?:

a.eql?(b)

Điều này hoạt động với các đối tượng Method trong ví dụ của chúng ta:

> a.method(:[]=).hash == a.method(:[]=).hash
=> true

Kết luận

Sau khi quen với các pattern phát triển web Ruby, thật thú vị khi nhìn source optcarrot và thấy một app non-web real-time dùng các pattern khác nhau. Trong web app tôi nghi ngờ tôi sẽ không bao giờ tạo mảng 65536 phần tử, nhưng ở đây, như một phần của setup cho một app "desktop", nó có nhiều ý nghĩa.

Nếu bạn có bất kỳ câu hỏi hoặc bình luận, hãy liên hệ tại starr@honeybadger.io hoặc @StarrHorne trên Twitter.