Layered and defaulting dictionaries
Two small dictionary utilities round out the dictionary family with shapes borrowed from Python's standard library: LayeredDictionary<TKey, TValue> is the .NET analogue of collections.ChainMap, and DefaultingDictionary<TKey, TValue> is the analogue of collections.defaultdict.
Layered dictionary (ChainMap)
LayeredDictionary<TKey, TValue> is a live, read-through view over an ordered list of underlying dictionaries. It copies nothing: lookups search the layers in order and the first layer containing the key wins, so an entry in an earlier layer shadows any same-keyed entry in later layers. All writes - Add, the indexer setter, Remove, Clear - go to the first layer only, exactly matching Python ChainMap.
This is the same first-wins precedence model as Bodu.Text.Configuration's resolver chain, where earlier configuration sources take precedence over later ones - a layered dictionary is the general-purpose collection expression of that idea: overrides in front, defaults behind.
using Bodu.Collections.Generic;
var overrides = new Dictionary<string, string>();
var defaults = new Dictionary<string, string> { ["colour"] = "blue", ["size"] = "medium" };
var settings = new LayeredDictionary<string, string>(overrides, defaults);
string colour = settings["colour"]; // "blue" - falls through to the defaults layer
settings["colour"] = "red"; // writes to overrides, shadowing the default
colour = settings["colour"]; // "red" - the first layer wins
Contract points worth keeping in mind:
The layer list is fixed at construction; the layers stay live. Mutating an underlying dictionary directly is immediately visible through the view. The
Layersproperty exposes the list read-only.Removemutates the first layer only. It returnsfalsefor a key that exists only in deeper layers - the key stays visible through the view. Removing a first-layer entry that shadowed a deeper one makes the deeper value visible again (the unshadowing behaviour):settings.Remove("colour"); // removes "red" from overrides… colour = settings["colour"]; // "blue" - the default is unshadowedAddthrows only when the first layer already contains the key. A key that exists solely in deeper layers is addable - the new first-layer entry simply shadows the deeper value.Clearclears the first layer only, so previously shadowed deeper entries become visible again.Count,Keys,Values, and enumeration present the merged view - distinct keys with first-wins values. They walk every layer, soCountis O(n) in the total entries across layers on every call, not a cached property.KeysandValuesare snapshots taken at call time; enumeration is lazy over the live layers.The optional comparer governs only the view's deduplication. Each underlying dictionary keeps resolving keys with its own comparer, so mismatched comparers can produce surprising shadowing (a case-insensitive first layer answers for casings the view considers distinct). Construct the view and its layers with the same comparer.
The non-generic
IDictionary/ICollectioninterfaces are deliberately not implemented - this is a view type, not a stand-alone collection.
Defaulting dictionary (defaultdict)
DefaultingDictionary<TKey, TValue> wraps a Dictionary<TKey, TValue> and bakes a Func<TKey, TValue> value factory into the type: reading a missing key through the indexer invokes the factory, stores the produced value, and returns it. Exactly as in Python - where __missing__ fires only for d[key] - the indexer getter is the only member that materializes defaults: TryGetValue, ContainsKey, Remove, Count, and enumeration see only entries that have actually been stored.
using Bodu.Collections.Generic;
var groups = new DefaultingDictionary<string, List<int>>(_ => new List<int>());
groups["odd"].Add(1); // miss - the factory creates the list, it is stored, then mutated
groups["odd"].Add(3); // hit - the stored list is returned; the factory is not invoked
bool has = groups.ContainsKey("even"); // false - lookups never materialize defaults
How it relates to the existing GetOrAdd extension in Bodu.Core: IDictionaryExtensions.GetOrAdd stays the lightweight per-call-site option - you pass the factory at each call against any IDictionary<TKey, TValue>. DefaultingDictionary<TKey, TValue> fixes the policy at construction, so every plain indexer read applies it; hand the dictionary to code that only knows IDictionary<TKey, TValue> and the defaults still materialize.
Contract points:
- The factory and the comparer are exposed through
ValueFactoryandComparer. - Reentrancy is deterministic: if the factory itself mutates the dictionary - even assigning the very key being materialized - the factory's return value is stored last and wins.
TValueis unconstrained: a factory returningnullstoresnullas-is; the key is then present and the factory is not invoked for it again.
Note
Neither type is thread-safe. The layered view's merged reads and the defaulting dictionary's check-invoke-store sequence both require external synchronization when shared across threads.
Where to go next
- LayeredDictionary<TKey, TValue> and DefaultingDictionary<TKey, TValue> - the full API surfaces.
- Choosing a collection - the full decision guide across the namespace.
- Sequenced dictionary, Bidirectional dictionary, Multi-value dictionary - the rest of the dictionary family.
- Bodu.Collections introduction - namespaces, headline types, scenarios.