Khi sử dụng Hash thường xuyên như các nhà phát triển Ruby, dễ nghĩ rằng đã biết hết về nó. Nhưng Hash giản dị vẫn ẩn chứa những thủ thuật thú vị, cho phép làm những việc tinh vi và mạnh mẽ.
Bất kỳ đối tượng nào cũng có thể là khóa Hash
Trước khi đi sâu, hãy lưu ý: dù thường dùng String và Symbol làm khóa, bạn hoàn toàn có thể dùng gần như bất kỳ đối tượng nào.
# Số làm khóa
{1 => "one"}[1] # "one"
# Kernel module làm khóa
{Kernel => 1}[Kernel] # 1
# Lưu giá trị cho class cụ thể
{Kernel => 1, String => 2}["hello world".class] # 2
# Boolean làm khóa
{true => "verdad"}[1==1] # "verdad"
# Thậm chí mảng phức tạp và Hash khác cũng làm khóa được
{[[1,0],[0,1]] => "identity matrix"}[[[1,0], [0,1]]] # "identity matrix"
Một số cách hữu ích hơn khác, nhưng tất cả đều khả thi.
Bạn kiểm soát được giá trị mặc định
Giả sử hash `h={ a: 1 }`. Truy cập khóa không tồn tại như `h[:x]` trả về `nil`, vì `nil` là giá trị mặc định trừ khi bạn chỉ định khác.
Có thể đặt giá trị mặc định khi khởi tạo:
h = Hash.new("This attribute intentionally left blank")
h[:a] = 1
h[:a] # 1
h[:x] # "This attribute intentionally left blank"
Giá trị mặc định động
Chú ý kỹ phần này, nó là nền tảng cho các kỹ thuật tiếp theo.
Nếu truyền block vào constructor, bạn có thể sinh giá trị mặc định theo chương trình. Ví dụ dưới thêm timestamp để thấy rõ việc sinh động:
h = Hash.new { |hash, key| "#{key}: #{ Time.now.to_i }" }
h[:a] # "a: 1435682937"
h[:a] # "a: 1435682941" (timestamp khác)
h[:b] # "b: 1435682943"
Quan trọng: block "giá trị mặc định" có thể làm nhiều hơn chỉ trả về giá trị.
Ném ngoại lệ khi khóa Hash không tồn tại
Vấn đề chính của Hash là thất bại lặng lẽ. Gõ nhầm `user[:phnoe]` thay vì `user[:phone]`, Hash trả về `nil` thay vì ném lỗi. Nhưng bạn có thể thay đổi hành vi này:
h = Hash.new { |hash, key| raise ArgumentError.new("No hash key: #{ key }") }
h[:a]=1
h[:a] # 1
h[:x] # raises ArgumentError: No hash key: x
Kỹ thuật này hữu ích cho debug và refactor vì áp dụng cho một Hash cụ thể, ít xâm lấn hơn monkey-patch class Hash.
Lưu ý: Không khuyên dùng thay cho `Hash.fetch` trong code mới. Chỉ là thủ thuật thú vị cho debug/refactor.
Bảng tra cứu lazy (tự động sinh)
Kỹ thuật này dùng để cache kết quả phép tính. Giả sử cần tính nhiều căn bậc hai, có thể tạo bảng tra cứu lazy:
sqrt_lookup = Hash.new { |hash, key| hash[key] = Math.sqrt(key) }
sqrt_lookup[9] # 3.0
sqrt_lookup[7] # 2.6457513110645907
sqrt_lookup # {9=>3.0, 7=>2.6457513110645907}
Bảng tra cứu lazy đệ quy
Giả sử có hàm đệ quy và muốn cache kết quả mỗi bước. Ví dụ tính giai thừa. "4!" = 4×3×2×1. Triển khai đệ quy với Hash:
factorial = Hash.new do |h,k|
if k > 1
h[k] = h[k-1] * k
else
h[k] = 1
end
end
factorial[4] # 24
factorial # {1=>1, 2=>2, 3=>6, 4=>24}
Thay đổi giá trị mặc định sau khởi tạo
Cũng có thể kiểm soát giá trị mặc định sau khi Hash đã tạo, dùng setter `default` và `default_proc`:
h={}
h[:a] # nil
h.default = "new default"
h[:a] # "new default"
h.default_proc = Proc.new { Time.now.to_i }
h[:a] # 1435684014
Tìm Ruby: Trò chơi Hash lồng nhau vô tận lazy
Chỉ để vui, gộp các kỹ thuật trên vào một ví dụ "vô cùng vô ích". Nhớ game Adventure dạng text cũ? Xây dựng phiên bản ngớ ngẩn nhất.
Bạn ở hang động, có thể đi bắc, nam, đông, tây. Ba lựa chọn dẫn đến "phòng" mới để tiếp tục khám phá. Một lựa chọn dẫn đến "ruby". Do đó tên game: "tìm ruby".
Mỗi phòng tương ứng một Hash. Hash chỉ có một entry. Một trong `["n", "s", "e", "w"]` ngẫu nhiên có giá trị "You found the ruby!". Chọn sai, Hash mới được tạo và thêm vào cây.
generator = Proc.new do |hash, key|
hash[key] = Hash.new(&generator).merge(["n", "s", "e", "w"][rand(4)] => "You found the ruby!")
end
dungeon = Hash.new(&generator)
dungeon["n"] #