A Performant Flexible Tree Structure with Type-Segregated Storage
When working with hierarchical or tree-structured data, developers often face a trade-off between flexibility and performance. Traditional approaches either use:
- Concrete types for strict schemas and fast access, but limited flexibility.
- Dynamic objects for flexible, schema-less data, but with runtime overhead and boxing of value types.
AdhocTree attempts to provide the best of both worlds. It's a tree data structure that combines the flexibility of dynamic objects with the performance benefits of type-specific storage.
What Is It?
AdhocTree is a tree-like data structure designed to store arbitrary named values at each node, supporting a wide range of value types. It achieves this by:
- Storing values in separate arrays by type (e.g., one array for
int
, another fordouble
, etc.), avoiding boxing of value types. - Maintaining a mapping from names to (type, index) pairs, so each name points to the correct array and position.
- Supporting recursive subnodes as first-class values, enabling hierarchical data.
- Providing fast, type-safe access without runtime binding or reflection.
- Offering a compaction method to reclaim memory by removing overwritten or unused values.
Why Use AdhocTree?
1. Performance Without Boxing
In many dynamic or dictionary-based structures, value types like integers or floats are stored as object
, causing boxing and heap allocations. AdhocTree stores each type in its own array, so value types remain unboxed and memory-efficient.
2. Flexible Schema
Unlike rigid concrete types, AdhocTree lets you add or overwrite named values of any supported type at runtime, without predefining classes or schemas.
3. Hierarchical Data
You can nest AdhocTree
instances inside each other, building complex trees with arbitrary depth and structure.
4. Memory Management
Over time, overwriting values leaves "dead" entries in arrays. AdhocTree provides a GetShrankCopy()
method to create a compacted copy, defragmenting and freeing memory.
How Does It Work?
Each node maintains:
- Multiple typed lists: one for each supported value type (
int
,double
,string
,AdhocTree
, etc.). - A dictionary mapping each name to a
(Type, index)
tuple, indicating which list and position holds the value.
When you Set
a value:
- If the name already exists with the same type, the value is updated in place.
- If the name exists with a different type, the old reference is nulled (to aid GC), and the new value is appended to the appropriate typed list.
- The mapping is updated accordingly.
When you Get
a value:
- The mapping is used to find the correct typed list and index.
- The value is returned with no boxing or dynamic dispatch.
Example Usage
var root = new AdhocTree(); root.Set("a", 123); root.Set("b", 3.14); root.Set("c", "hello"); root.Set("d", new DateTime(2025, 7, 30));
assert(root.Get("a", out int intA)); assert(123 == intA); assert(root.Get("b", out double doubleB)); assert(3.14 == doubleB); assert("hello" == root.Get<string>("c")); assert(new DateTime(2025, 7, 30) == (DateTime?)root.Get("d"));
// Overwrite with different type. root.Set("a", 2.718); assert(root.Get("a", out double doubleA)); assert(2.718 == doubleA);
// Check subnodes. var node = root.AddNode("MyNode"); node.Set("x", 42);
assert(root.GetNode("MyNode", out var n)); assert(node == n); assert(n.Get("x", out int intX)); assert(42 == intX);
// Shrink the tree and check that the values are kept. var next = root.GetShrankCopy();
assert(next.Get("a", out doubleA)); assert(2.718 == doubleA); assert(next.Get("b", out doubleB)); assert(3.14 == doubleB); assert("hello" == next.Get<string>("c")); assert(new DateTime(2025, 7, 30) == (DateTime?)next.Get("d"));
// Check subnodes. assert(next.GetNode("MyNode", out var subnode)); assert(node != subnode); assert(n.Get("x", out intX)); assert(42 == intX);
Compacting the Tree
Over time, overwriting values leaves unused entries in the internal arrays. To reclaim memory, you can create a compacted copy:
var compacted = root.GetShrankCopy();
This returns a new AdhocTree
instance containing only the currently referenced values, recursively compacting all subnodes.
When to Use AdhocTree?
- You need dynamic, schema-less hierarchical data.
- You want to avoid boxing overhead for value types.
- You want fast, type-safe access without reflection or dynamic binding.
- You want to manage memory efficiently in long-lived or frequently updated trees.
Summary
AdhocTree is a hybrid data structure that blends the flexibility of dynamic dictionaries with the performance of typed arrays. It’s ideal for scenarios where you want to store complex, hierarchical data with mixed types, but still care about runtime efficiency and memory usage.
If you’re building applications involving dynamic configurations, hierarchical metadata, or flexible data models, AdhocTree might be the perfect fit!