weak_map 4.1.2
weak_map: ^4.1.2 copied to clipboard
WeakMap is a map whose keys are weakly held, to attach data to objects without keeping them alive. Cache functions for memoization over immutable state.
Sponsor
weak_map #
This package gives you:
-
WeakMap: A map whose keys are weakly held. You use it to attach values to objects without keeping those objects alive.
-
Cache functions: Memoization of expensive calculations over immutable state, with the cached results discarded when the state changes or is no longer used. They are similar to the selectors of the reselect package, but better:
cache1state,cache1state_1param,cache1state_2params,cache2states,cache2states_1param,cache2states_2params,cache2states_3params,cache3states,cache1state_0params_x,cache2states_0params_x,cache3states_0params_x.
First, Dart's own WeakReference #
When this package was created, Dart had no weak references. Since Dart 2.17, it has one built in: WeakReference.
A WeakReference points to a single object without keeping that object alive. While
other
parts of your program use the object, target returns it. After the object is
garbage-collected, target returns null:
var user = User("John");
var ref = WeakReference(user);
// User("John"), while the user is still in use.
print(ref.target);
// Later, if nothing else uses the user, it may be garbage-collected:
print(ref.target); // null
Use it when one object needs to point to another object, but shouldn't be the reason that object stays in memory. Examples are a child pointing back to its parent, a listener registry that shouldn't keep the listeners alive, or a "last seen" object you'll reuse if it's still around.
Things to know:
-
You must check
targetfornullevery time you use it, since the garbage-collector can run at any moment. -
WeakReferencethrows anArgumentErrorfor numbers, Strings, booleans, records andnull. -
WeakReferencecompares by identity, so two references to the same object are not equal (WeakReference(a) != WeakReference(a)). -
If you need to run some code when the object is collected, use Dart's Finalizer.
WeakMap #
A WeakMap is a map whose keys are held weakly. If you use some object as a key, the
map alone won't keep that object alive. After all other references to the key are gone,
the key may be garbage-collected, and its whole entry (key and value) disappears from
the map.
var map = WeakMap<User, Avatar>();
map[user] = avatar; // Add.
var a = map[user]; // Read. Same as map.get(user).
map.contains(user); // true
map.remove(user); // Remove.
map.clear(); // Remove everything.
Note only the keys are weak. The values are kept alive for as long as their keys are alive.
Why it's useful #
A WeakMap lets you associate data with objects you don't control, without having to
remember to clean it up. For example:
-
Metadata. You want to attach some extra information to objects that belong to another library, or to classes you can't change. With a regular
Map, you'd have to remove the entry when the object is no longer used, or the map would grow forever. With aWeakMap, the entry goes away by itself. -
Caching results per object. You calculate something expensive from an immutable object (a layout, a parsed version, a filtered list), and want to reuse it while that object is still around. The cache functions of this package are built on this idea.
-
Tracking objects. You want to mark objects as "already processed", "visited" or "seen", without keeping them in memory after everyone else stopped using them.
WeakMap vs. WeakReference #
Neither of them keeps objects alive, but they solve different problems:
-
A
WeakReferenceis a pointer to one object. It answers: "Is this object still alive, and if so, give it to me." -
A
WeakMapis a lookup table from objects to values. It answers: "I have this object here. What value did I attach to it?"
You could try to build a weak map yourself with Map<WeakReference<User>, Avatar>, but it
doesn't work well:
-
You can't look things up.
WeakReferenceuses identity for==, somap[WeakReference(user)]creates a new reference that never matches the one in the map. You'd have to key the map byidentityHashCode(user)and then handle hash collisions yourself. -
Dead entries pile up. When a key is collected, its entry stays in the map, with a
WeakReferencewhosetargetis nownull, and a value that is still taking up memory. You'd need aFinalizer, or periodic sweeps, to remove them. -
Values can keep their own keys alive. If the value references its key (which is common, for example
map[user] = Avatar(owner: user)), then the regular map keeps the value alive, the value keeps the key alive, and the key is never collected. That's a memory leak.WeakMapdoesn't have this problem: a value only stays alive because of its key, and never keeps that key alive.
WeakMap takes care of all of this for you. Internally it uses Dart's
Expando, which is
supported by the garbage-collector itself.
When to use which #
| You want to | Use |
|---|---|
| Point to one object without keeping it alive, and get it back later | WeakReference |
| Check if some object is the same one you had before, without keeping it alive | WeakReference, with identical(ref.target, obj) |
| Run some code when an object is garbage-collected | Finalizer |
| Attach values to objects, and have those values go away with the objects | WeakMap |
| Cache expensive calculations over immutable state | The cache functions (see below) |
| Iterate over the entries, or know how many there are | A regular Map (see note 6 below) |
Notes #
-
Object keys are compared by identity, not by
operator ==. Two different objects that are equal are two different keys. -
If you use
null, a number, a boolean, a String or a record as a key, theWeakMapacts like a regular map for that key: these values are never garbage-collected, and they are compared by equality (==). Constant objects (const) are compared by identity, and are never garbage-collected either.Note, a Dart record key keeps the objects inside it alive, until you remove it or clear the map. Records can't be weak keys, because they have no identity. If you need a weak composite key, nest the maps instead. The entry then goes away when either object is garbage-collected:
// Instead of WeakMap<(User, Doc), Result> var map = WeakMap<User, WeakMap<Doc, Result>>(); -
Setting a key to
nullis the same as removing it:map[user] = nullis the same asmap.remove(user). For the same reason,containsreturnsfalsefor keys set tonull. -
map[key]andmap.get(key)returnnullif the key doesn't exist. Usemap.getOrThrow(key)to throw aStateErrorinstead. -
map[key] = valueis the same asmap.add(key: key, value: value). -
A
WeakMaphas nolength,keys,valuesor iteration, on purpose. Whether an entry is still there depends on when the garbage-collector runs, which is unpredictable. And since you can only look up an entry while you still have its key, which means the key is alive, you can never observe an entry disappearing. This keeps your program's behavior the same, no matter when (or if) the garbage-collector runs.
Cache #
Suppose you have some immutable information, which we call "state", and some parameters. We want to perform some expensive process (calculation, selection filtering etc) over the state, and we want to cache the result.
For example, suppose you want to filter an immutable list of millions of users, to create a new list with only the names that start with some text. You could filter the users list to remove all other names, like this:
List<User> filter(String text) =>
users.where((user) => user.name.startsWith(text)).toList();
This is an expensive process, so you may want to cache the filtered list.
In this example, we have a single state and a single parameter, so we're going to use
the cache1state_1param method:
static List<User> filter(Users users, String text)
=> _filter(users)(text);
static final _filter = cache1state_1param((Users users) => (String text)
=> users.where((user)=>user.name.startsWith(text)).toList());
The above code will calculate the filtered list only once, and then return it when the
filter function is called again with the same users and text.
If the function is called with a different users and/or text, it will recalculate
and cache the new result.
However, it treats the state and the parameter differently. If you call the function while keeping the same state and changing only the parameter, it will cache all the results, one for each parameter.
However, as soon as you call the function with a changed state, it will delete all of its previous cached information, since it understands that they are no longer useful.
And even if you don't call that function ever again, it will delete the cached information if it detects that the state is no longer used in other parts of the program. In other words, it keeps the cached information in a weak-map, so that the cache will not hold to old information and have a negative impact in memory usage.
States are compared by identity if they are objects, and by equality if they are
numbers, Strings, booleans, records or null. Parameters are compared by equality (==).
This is why the state must be immutable: if you change the contents of the state object,
the cache can't notice it, and will keep returning the old result.
If a state is a record with objects inside, those objects are kept alive until the state
changes.
Some functions, marked with an "x", also let you pass some extra information which is not used in any way to decide whether the cache should be used/recalculated/evicted.
For the moment, the following 11 methods are provided, which combine 1, 2 or 3 states with 0, 1, 2 or 3 parameters, and possibly some extra information:
cache1state((state) => () => ...);
cache1state_1param((state) => (parameter) => ...);
cache1state_2params((state) => (parameter1, parameter2) => ...);
cache2states((state1, state2) => () => ...);
cache2states_1param((state1, state2) => (parameter) => ...);
cache2states_2params((state1, state2) => (parameter1, parameter2) => ...);
cache2states_3params((state1, state2) => (parameter1, parameter2, parameter3) => ...);
cache3states((state1, state2, state3) => () => ...);
cache1state_0params_x((state1, extra) => () => ...);
cache2states_0params_x((state1, state2, extra) => () => ...);
cache3states_0params_x((state1, state2, state3, extra) => () => ...);
I have created only those above, because for my own usage I never required more than that. Please, open an issue to ask for more variations in case you feel the need.
Note: These cache functions are similar to the "createSelector" functions found in the reselect package. The differences are: First, here you can keep any number of cached results for each function, one for each time the function is called with the same state and different parameters. Meanwhile, the reselect package only keeps a single cached result per function. Second, here it discards the cached information when the state changes or is no longer used in other parts of the program. Meanwhile, the reselect package will always keep the states and cached results in memory.
By Marcelo Glasberg #
glasberg.dev
github.com/marcglasberg
linkedin.com/in/marcglasberg/
twitter.com/glasbergmarcelo
stackoverflow.com/users/3411681/marcg
medium.com/@marcglasberg
My article in the official Flutter documentation:
The Flutter packages I've authored:
- async_redux
- provider_for_redux
- i18n_extension
- align_positioned
- network_to_file_image
- image_pixels
- matrix4_transform
- back_button_interceptor
- indexed_list_view
- animated_size_and_fade
- assorted_layout_widgets
- weak_map
- themed
- bdd_framework
- tiktoken_tokenizer_gpt4o_o1
My Medium Articles:
