skip to content

In Ruby, how does ObjectSpace::WeakMap differ from a Hash and from ObjectSpace::WeakKeyMap when you use it as a cache?

level: middleimportance: nice to knowfreq 15%

answer

  1. entries vanish after collection
  2. WeakMap: weak keys and values
  3. WeakMap compares keys by identity
  4. WeakKeyMap: eql? keys, strong values
  5. WeakKeyMap#getkey for canonical objects

basics

~20 s

A Hash keeps keys and values alive. ObjectSpace::WeakMap holds both weakly and compares keys by identity, so entries vanish once either is collected. ObjectSpace::WeakKeyMap, since Ruby 3.3, holds only keys weakly, keeps values strongly and compares keys with eql?.

solid answer

~40 s

A `Hash` used as a cache keeps every key and value reachable, so it grows until you evict. `ObjectSpace::WeakMap` holds **both keys and values weakly** and compares keys by **identity**: once nothing else references the key or the value, the GC can drop the entry, and `map[key]` then returns `nil`. Identity comparison means a different but equal String will not find an entry. `ObjectSpace::WeakKeyMap`, added in Ruby 3.3, holds **only keys** weakly, keeps **values strongly**, and compares keys with `eql?` and `hash`, which suits attaching data to objects you do not own or deduplicating value objects with `getkey`. Both maps are CRuby-specific tools; entries disappear only when a collection actually runs, so code must always handle a miss.

code

ruby · 11 lines
ruby
wm = ObjectSpace::WeakMap.new
key = +"report"
wm[key] = Object.new

p wm[key]            # => #<Object:...>
p wm[+"report"]      # => nil: equal String, different object

wk = ObjectSpace::WeakKeyMap.new
wk[key] = :cached
p wk[+"report"]      # => :cached: keys compared with eql?
p wk.getkey(+"report").equal?(key)  # => true

go deeper

for a junior

Recall that a Hash keeps what it holds alive while a weak map lets the GC drop entries whose objects are no longer used.

for a middle

Explain WeakMap's weak keys and values with identity comparison versus WeakKeyMap's weak keys, strong values and eql? comparison.

for a senior

Show when a weak map beats an explicit bound, why misses must always be handled, and why tests must not assert eviction.

for a principal

Decide where cache lifetimes should follow object lifetimes and where the team needs explicit, observable eviction instead.

## Three maps, three lifetimes | | `Hash` | `ObjectSpace::WeakMap` | `ObjectSpace::WeakKeyMap` | |---|---|---|---| | Keys held | strongly | weakly | weakly | | Values held | strongly | weakly | strongly | | Key comparison | `eql?` and `hash` | identity | `eql?` and `hash` | | Entry disappears when | you delete it | key or value is collected | key is collected | | Since | always | long-standing | Ruby 3.3 | "Weakly" means the map's own reference does not keep the object alive: if nothing else in the program references it, the garbage collector may reclaim it and the entry goes away. ## ObjectSpace::WeakMap `WeakMap` is the older of the two. Its documentation states that it holds weak references to its keys **and** values and compares keys **by identity**. Its interface is Hash-like but smaller: - `[]=`, `[]`, `key?` / `include?` / `member?`; - `delete` (added in Ruby 3.3 to clear an entry eagerly); - `each`, `each_key`, `each_value`, `keys`, `values`, `size`. Consequences worth stating: 1. **Identity keys**: `map["name"] = x` followed by `map["name"]` with a *different* String object returns `nil`, even though the strings are equal. 2. **Either side can drop the entry**: if the value has no other references, the entry disappears even while the key is alive. 3. **Misses are normal**: code must treat `nil` as "rebuild it", never as an error. A typical use is a cache from an object you keep alive elsewhere to a derived object you can recompute. ## ObjectSpace::WeakKeyMap `WeakKeyMap`, added in Ruby 3.3, fixes the two things that make `WeakMap` awkward: - keys are compared **by value** with `eql?`, like a Hash; - **values are strong**, so they stay as long as the key lives. Only garbage-collectable objects can be keys; a Symbol, Integer or Float key raises `ArgumentError`. It offers `[]=`, `[]`, `key?`, `delete`, `clear` and `getkey`. `getkey(obj)` returns the **existing key** that is `eql?` to `obj`, which enables a documented pattern: keep one canonical instance of each equal value object and let unused ones be collected. ## When a weak map is the wrong tool - **Bounded caches with an eviction policy** - a weak map evicts on GC timing, not on size or age; for predictable memory use an explicit bound. - **Uncollectable keys** - a weak map gains nothing when its keys never die, such as small Integers; `WeakKeyMap` goes further and raises `ArgumentError` ("WeakKeyMap keys must be garbage collectable") for Symbols, Integers and Floats. - **Tests relying on eviction** - the class documentation itself notes that `GC.start` in its examples "might not always lead to demonstrated results"; never assert that an entry is gone right after `GC.start`. ## Summary `WeakMap`: weak keys and values, identity comparison. `WeakKeyMap`: weak keys, strong values, `eql?` comparison, plus `getkey`. A `Hash` keeps everything until you remove it. Choose a weak map when the entry's lifetime should follow an object you do not control, and handle misses every time.

  • Why can a WeakMap entry disappear while its key is still in use?
    `WeakMap` holds values weakly too. If the value object has no other references, the collector may reclaim it and the entry is gone even though the key is alive. Keep a strong reference to the value elsewhere, or use `WeakKeyMap`, whose values are strong.
  • What is WeakKeyMap#getkey for?
    It returns the existing key that is `eql?` to the argument, or `nil`. That lets you keep one canonical instance of each equal value object: look it up with `getkey`, return the stored one if present, otherwise store the new one. Unused canonical instances are still collected because keys are weak.

saying these in an interview costs you the question

  • WeakMap compares keys with eql? just like a Hash
  • WeakKeyMap holds its values weakly as well as its keys
  • After GC.start, a weak map entry is guaranteed to be gone
  • A weak map is a good bounded cache with predictable size
  • WeakMap entries can only disappear when the key is collected