Features Utilities Data Structures - Ambiguous-Interactive/unity-helpers GitHub Wiki
- Pick the right container for performance and clarity: ring buffers, deques, heaps, tries, sparse sets, etc.
- Each section gives a plain‑language “Use for / Pros / Cons” and a tiny code snapshot to copy.
- When in doubt, prefer simple .NET types; use these when you hit performance or ergonomics limits.
This guide covers several foundational data structures used across the library and when to use them.
- What it is: Fixed-capacity circular array with head/tail indices that wrap.
- Use for: Streaming data, fixed-size queues, audio/network buffers.
- Operations: enqueue/dequeue in O(1); overwriting old data optional.
- Pros: Constant-time, cache-friendly, no reallocations at steady size.
- Cons: Fixed capacity unless resized; must handle wrap-around.
When to use vs. DotNET queues
- Prefer
CyclicBuffer<T>overQueue<T>when you want bounded memory with O(1) push/pop at both ends and predictable behavior under backpressure (drop/overwrite oldest, or pop proactively). - Use
Queue<T>when you need unbounded growth without wrap semantics.
API snapshot
using WallstopStudios.UnityHelpers.Core.DataStructure;
var rb = new CyclicBuffer<int>(capacity: 4);
rb.Add(10); rb.Add(20); rb.Add(30);
// Pop from front/back
if (rb.TryPopFront(out var first)) { /* first == 10 */ }
if (rb.TryPopBack(out var last)) { /* last == 30 */ }
// Remove by value / predicate
rb.Add(40); rb.Add(50);
rb.Remove(20); // O(n) compacting via temp buffer
rb.RemoveAll(x => x % 2 == 0); // remove evens
// Resize in place (may drop tail elements)
rb.Resize(8);Tips and pitfalls
- Know your overflow policy.
Addwill wrap and overwrite the oldest only once capacity is reached; useTryPopFrontperiodically to keep buffer from evicting data you still need. - Iteration enumerates logical order starting at the head, not the underlying storage order.
-
Remove/RemoveAllare O(n); keep hot paths toAdd/TryPop*when possible.
- What it is: Queue with efficient push/pop at both ends.
- Use for: Sliding windows, BFS frontiers, task schedulers.
- Operations: push_front/push_back/pop_front/pop_back in amortized O(1).
- Pros: Flexible ends; generalizes queue and stack behavior.
- Cons: Implementation complexity for block-based layouts.
When to use vs Queue<T> / Stack<T>
- Prefer
Deque<T>when you need bothpush_frontandpush_backin O(1) amortized. - Use
Queue<T>for simple FIFO;Stack<T>for LIFO only.
API snapshot
using WallstopStudios.UnityHelpers.Core.DataStructure;
var dq = new Deque<string>(capacity: 8);
dq.PushFront("start");
dq.PushBack("end");
if (dq.TryPopFront(out var a)) { /* a == "start" */ }
if (dq.TryPopBack(out var b)) { /* b == "end" */ }
// Peeking without removal
dq.PushBack("x");
if (dq.TryPeekFront(out var f)) { /* f == "x" */ }Tips
- Capacity grows geometrically as needed; call
TrimExcess()after spikes to return memory. - Indexer is in logical order (0 is front, Count-1 is back).
-
CopyTo(array, arrayIndex)writes in logical order and leaves elements outside the destination range unchanged. Copies of at least 64 elements use one or two bulk copies when the destination has exactly the deque's element type. Smaller copies and covariant destination arrays retain element-by-element writes. -
ToArray(ref array)reuses a sufficiently large destination and leaves its unused tail unchanged.ToArray()allocates its result array.
- What it is: Array-backed binary tree maintaining heap-order (min/max).
- Use for: Priority queues, Dijkstra/A*, event simulation.
- Operations: push/pop in O(log n); peek O(1); build-heap O(n).
- Pros: Simple; great constant factors; contiguous memory.
- Cons: Not ideal for decrease-key unless augmented.
When to use vs SortedSet<T>
- Prefer
Heap<T>/PriorityQueue<T>for frequent push/pop top in O(log n) with low overhead. - Use
SortedSet<T>for ordered iteration and fast remove arbitrary item (by key), at higher constants.
API snapshot (Heap)
using WallstopStudios.UnityHelpers.Core.DataStructure;
var minHeap = new Heap<int>(); // default comparer => min-heap
minHeap.Add(5); minHeap.Add(2); minHeap.Add(9);
if (minHeap.TryPop(out var top)) { /* top == 2 */ }
if (minHeap.TryPeek(out var peek)) { /* peek == 5 */ }API snapshot (PriorityQueue)
using WallstopStudios.UnityHelpers.Core.DataStructure;
var pq = PriorityQueue<(int priority, string job)>.CreateMin(
capacity: 32
);
pq.Enqueue((1, "emergency"));
pq.Enqueue((5, "later"));
pq.TryDequeue(out var item); // (1, "emergency")Tips
- Use
PriorityQueue<T>.CreateMax()to flip ordering without writing a custom comparer. - Heaps don’t support efficient decrease-key out of the box; reinsert updated items instead.
- What it is: Structure tracking partition of elements into sets.
- Use for: Connectivity, Kruskal’s MST, percolation, clustering.
- Operations: union/find in amortized near O(1) with path compression + union by rank.
- Pros: Extremely fast for bulk unions/finds; minimal memory.
- Cons: Not suited for deletions or enumerating members without extra indexes.
When to use
- Batch connectivity queries where the graph mutates only via unions (no deletions): MST (Kruskal), island labeling, clustering, grouping by equivalence.
API snapshot (int-based)
using WallstopStudios.UnityHelpers.Core.DataStructure;
var uf = new DisjointSet(n: 6); // elements 0..5
uf.TryUnion(0, 1);
uf.TryUnion(2, 3);
uf.TryIsConnected(0, 3, out var conn); // false
uf.TryUnion(1, 3);
uf.TryIsConnected(0, 3, out conn); // trueAPI snapshot (generic)
using WallstopStudios.UnityHelpers.Core.DataStructure;
var people = new[] { "Ana", "Bo", "Cy" };
var uf = new DisjointSet<string>(people);
uf.TryUnion("Ana", "Bo");
uf.TryIsConnected("Ana", "Cy", out var conn); // falseTips
- Use the generic variant to work with domain objects; internally it maps to indices.
- No deletions: rebuild if you need dynamic splits.
- What it is: Two arrays (sparse and dense) enabling O(1) membership checks and iteration over active items.
- Use for: ECS entity sets, fast presence checks with dense iteration.
- Operations: insert/remove/contains in O(1); iterate dense in O(n_active).
- Pros: Very fast, cache-friendly on dense array; stable indices optional.
- Cons: Requires ID space for indices; sparse array sized by max ID.
When to use vs HashSet<T>
- Prefer
SparseSetwhen your IDs are small integers (0..N) and you need O(1) contains with dense, cache-friendly iteration over active items. - Use
HashSet<T>for arbitrary keys, very large/unbounded key spaces, or when memory forsparsecannot scale to the max ID.
API snapshot (int IDs)
using WallstopStudios.UnityHelpers.Core.DataStructure;
var set = new SparseSet(capacity: 1000); // supports IDs in [0,1000)
set.TryAdd(42); // returns bool indicating success
bool has42 = set.Contains(42); // true
set.TryRemove(42); // returns bool indicating success
// Dense iteration order over active IDs
foreach (int id in set) { /* ... */ }API snapshot (generic values)
public sealed class MyComponent { }
var set = new SparseSet<MyComponent>(capacity: 1024);
set.TryAdd(new MyComponent()); // the element IS the key, returns bool
var comp = set[0]; // dense index 0 valueTips
- Capacity equals the universe size for keys; do not set capacity larger than your maximum possible ID.
- Deletions swap-with-last in dense array; dense order is not stable.
- What it is: Tree keyed by characters for efficient prefix-based lookup.
- Use for: Autocomplete, spell-checking, dictionary prefix queries.
- Operations: insert/search O(m) where m is key length.
- Pros: Predictable per-character traversal; supports prefix enumeration.
- Cons: Memory overhead vs hash tables; compact with radix/compressed tries.
When to use vs dictionaries
- Prefer
Triefor lots of prefix queries and auto-complete where per-character traversal beats repeated hashing. - Use
Dictionary<string, T>when you rarely do prefix scans and primarily need exact lookup.
API snapshot
using WallstopStudios.UnityHelpers.Core.DataStructure;
// Words only
var words = new Trie(new[] { "cat", "car", "dog" });
bool hasDog = words.Contains("dog"); // true
List<string> outWords = new();
words.GetWordsWithPrefix("ca", outWords); // outWords = ["cat","car"]
// Key -> Value
var items = new Dictionary<string, int> { ["apple"] = 1, ["apricot"] = 2 };
var trie = new Trie<int>(items);
if (trie.TryGetValue("apricot", out var v)) { /* 2 */ }
List<int> values = new();
trie.GetValuesWithPrefix("ap", values); // values = [1,2]Tips
- Build once with full vocabulary; Tries here are immutable post-construction (no public insert) to stay compact.
- Memory scales with total characters; very large alphabets or long keys benefit from compressed/radix tries (not included here).
- What it is: Packed array of bits for boolean sets and flags.
- Use for: Fast membership bitmaps, masks, filters, small Bloom filters.
- Operations: set/clear/test O(1); bitwise ops on words are vectorizable.
- Pros: Extremely compact; very fast bitwise operations.
- Cons: Fixed maximum size unless dynamically extended; needs index mapping.
When to use vs bool[] / HashSet<int>
- Prefer
BitSetfor dense boolean sets with fast bitwise ops (masks, layers, filters) and compact storage. - Use
bool[]for tiny, fixed schemas you manipulate rarely; useHashSet<int>for sparse, very large universes.
API snapshot
using WallstopStudios.UnityHelpers.Core.DataStructure;
var bits = new BitSet(initialCapacity: 128);
bits[3] = true; // indexer calls TrySet/TryClear
bits.TrySet(64);
bool any = bits.Any(); // any bit set?
int count = bits.CountSetBits();
// Bitwise ops
bits.Not(); // invert
bits.LeftShift(2); // multiply-by-4 mask window
bits.RightShift(1);Tips
- Capacity grows automatically when setting beyond bounds; prefer sizing appropriately upfront for fewer resizes.
TrySetandTryFlipreturnfalsefor negative indices and forint.MaxValue, whose required capacity cannot be represented; indexer assignments at those indices do nothing. - Left/Right shift drop/zero-fill at the edges; use with care if capacity is small.
-
LeftShiftupdates words in place without temporary storage or pool activity. It preserves capacity and independent immutable snapshots, ignores nonpositive shifts, and clears the set when the shift reaches capacity.
- What it is:
IntMap<TValue>, a hash table with linear probing, Fibonacci hashing from the high product bits, a power-of-two table and keys compared as raw integers, with noIEqualityComparerindirection anywhere on the lookup path. - Use for: read-mostly lookups keyed by entity ids, network ids, slot indices or any dense/sparse int id.
- Operations:
TryGet,TrySet, indexer,Remove,Clear, enumeration; growth at a 0.25 live-entry load factor; removal closes probe-chain gaps in place without allocating or rebuilding the table. - Pros: direct integer comparison and value enumerators avoid comparer dispatch and typed enumeration boxing. Measure lookup speed for your own key distribution and miss rate.
- Cons: misses probe until an empty slot, so measure the intended miss rate; the two lowest key values (
int.MinValueand one above) are reserved, refused byTrySetwithout throwing.
using WallstopStudios.UnityHelpers.Core.DataStructure;
var map = new IntMap<HealthState>();
map.TrySet(entityId, HealthState.Poisoned);
if (map.TryGet(entityId, out HealthState state)) { /* ... */ }
map.TrySet(entityId, HealthState.Healthy); // overwrite in place
map.Remove(entityId, out _); // closes the probe-chain gap without allocatingTips and pitfalls
-
TryGetchecks each stored key directly and returns its value on a match. It uses the same high-bit hash and wrapping probe sequence as insertion and removal. - A fresh Unity 6000.4.6f1 Release Windows Mono player measured 2.34x and 2.90x Dictionary speed for hit-only lookups at 1,000 and 10,000 keys, and 1.53x and 1.83x with 50% misses. All eight comparisons retained 32 samples per arm and met the 3% spread limit with a 600 ms calibration target. These measurements used an Intel Core Ultra 9 285K, CPU 12 affinity, elevated process/thread priority, and explicit High QoS. All four shared-low-bit comparisons stayed within twice the dense lookup time. See the complete acceptance evidence for the method and retained failed experiments; measure your own runtime and key distribution.
- Earlier editor Mono measurements reported 1.97x–2.19x hit-heavy and 1.26x–1.65x at a 50% miss rate using the previous low-bit slot selection. Those margins do not describe the corrected implementation.
IntMapPerformanceTestsretains calibrated counterbalanced comparisons; measurements whose spread exceeds 3% cannot establish acceptance. -
Those historical numbers are editor Mono only. IL2CPP has not been measured, and part of the margin is Mono not devirtualizing
EqualityComparer<int>.Default— which an AOT compiler may do. See #578. - Removal closes gaps by shifting displaced entries whose probe paths cross the deleted slot, including chains that wrap around the table. It leaves no tombstones and allocates no replacement arrays. Remove-before-add sliding windows can therefore retain one quarter of the table as live entries without growth or cleanup allocations; an eight-entry sliding window stays at the default 64 slots even after one million insertions. The table doubles only when adding another live entry would exceed its 0.25 load factor. The default table remains 64 slots; explicit capacity hints reserve at least four slots per requested live entry, with a minimum of 16 slots. Compared with the previous 0.5 limit, larger tables usually use twice the key and value storage to reduce collisions.
- Replacing an existing value through
TrySetor the indexer keeps the current storage, including at the occupancy limit and after removals. Only adding an absent key beyond the live-entry load limit triggers growth. - Keys sharing low bits, such as
i * 1024, use the high bits of the hash product for their starting slots. The performance fixture compares hits and misses against dense keys at 64 and 1,024 entries; stable sparse-key measurements must stay within twice the dense lookup time. - Keys below
IntMap<TValue>.MinimumAllowedKeyremain reserved and are refused byTrySet; the indexer throws for them.
Run npm run test:intmap for the same map correctness tests on the host .NET runtime without starting Unity. This gives a fast local edit loop; it does not establish Unity player compatibility or performance.
- What it is:
StringWrapper, a shared wrapper around a string, so a key you look up repeatedly costs one reference comparison in the common case instead of a character-by-character compare. - Use for: a known set of keys -- animator state names, tags, layer names, localization ids.
- Operations:
Get,Remove,Clear,CachedCount;Equals,GetHashCodeandCompareToare all by ordinal value. - Pros: every caller asking for the same string gets the same instance; the cache is bounded, so it cannot grow without limit.
- Cons: the cache is process-wide and shared, so eviction and
Removeact on behalf of every holder.
using WallstopStudios.UnityHelpers.Core.DataStructure;
StringWrapper alert = StringWrapper.Get("Enemy/State/Alert");
stateHandlers[alert] = OnAlert;
StringWrapper.MaxCachedWrappers = 8192; // default 4096; 0 or less removes the bound
StringWrapper.Remove("Enemy/State/Alert");Tips and pitfalls
-
The cache is bounded and eviction is silent. It holds at most
StringWrapper.MaxCachedWrappersstrings and evicts the least recently requested one to stay there. Without that bound, wrapping a value derived from gameplay -- an entity id, a save slot name, a$"{prefix}:{index}"-- kept every string a session ever built alive for the process (#694). - Eviction is safe because nothing in the contract depends on reference identity: equality and hashing are by ordinal value, so a wrapper handed out after an eviction still equals one handed out before it, and a dictionary keyed on the old instance still finds its entry with the new one. It costs one allocation on the next
Getfor that string. -
Do not wrap unbounded keys. The bound stops the leak; it does not make an unbounded key set fast, because every
Getthen misses and allocates. Wrap the fixed vocabulary, not the values flowing through it. -
Disposeis an obsolete no-op. The wrapper is shared, so one borrower'susingblock must not evict what every other borrower is reading; useRemoveorClearto administer the cache deliberately.
- Need O(1) membership and dense iteration: Sparse Set
- Need priority scheduling: Binary Heap
- Need two-ended queueing: Deque
- Need circular fixed-capacity buffer: Cyclic Buffer
- Need prefix search: Trie
- Need compact boolean set: Bitset
- Need dynamic connectivity: Disjoint Set
- Need auto-evicting key-value store: Cache
- Need direct integer-key comparison for read-mostly lookups: benchmark IntMap
Common pitfalls
- Sparse Set capacity equals the max key + 1; allocating for huge key spaces is memory-heavy.
- Heaps don’t give you sorted iteration; popping yields order, but enumerating the heap array is not sorted.
- Cyclic Buffer
Remove/RemoveAllare O(n); keep hot paths to TryPop/Add.
- Cyclic Buffer: enqueue/dequeue O(1)
- Deque: push/pop ends amortized O(1)
- Heap: push/pop O(log n), peek O(1)
- Disjoint Set: union/find ~O(1) amortized (with heuristics)
- Sparse Set: insert/remove/contains O(1), iterate dense O(n_active)
- Trie: insert/search O(m)
- Bitset: set/test O(1), bitwise ops O(n/word_size)
- Cache: get/set/remove O(1), expiration scan O(n)
- IntMap: TryGet/TrySet/Remove amortized O(1), O(1) worst-case memory per slot; speed depends on hit rate
Notes on constants
- All structures are allocation-aware (enumerators avoid boxing; internal buffers reuse pools where applicable). Real-world throughput is often more important than asymptotic notation; these implementations are tuned for Unity/IL2CPP.
- What it is: High-performance key-value cache with configurable eviction policies and time-based expiration.
- Use for: Memoization, asset lookups, network response caching, session data.
- Operations: get/set/remove in O(1); supports weighted entries, auto-loading, and statistics.
- Pros: Multiple eviction strategies; fluent builder API; jitter for thundering herd prevention.
- Cons: Memory overhead for tracking access patterns; requires configuration for optimal performance.
Cache expiration compares elapsed time with the stored lifetime, including the exact lifetime
boundary. Positive lifetimes retain entries on repeated reads at the same finite clock value,
including very large custom clocks. Small clock offsets and jitter remain significant beside very
large lifetimes, and finite jitter cannot make a finite lifetime infinite. When no explicit lifetime or custom lifetime function is
supplied, disabling both write and access expiration leaves entries without a deadline, even at
float.MaxValue.
An explicit positive lifetime overrides the custom lifetime function, which overrides write
expiration. Access expiration also bounds the first read; a successful read resets the lifetime to
the sliding access interval. Moving a finite clock backward does not expire an entry with a
positive lifetime. Custom NaN and infinite lifetimes retain their existing comparison behavior.
If a custom lifetime function throws, its exception propagates before values, expiry schedules,
storage slots, or capacity evictions change.
flowchart LR
subgraph Cache Operations
Get[Get] --> Hit{Hit?}
Hit -->|Yes| Return[Return Value]
Hit -->|No| Load[Load/Miss]
Set[Set] --> Evict{At Capacity?}
Evict -->|Yes| Policy[Apply Eviction Policy]
Evict -->|No| Store[Store Entry]
Policy --> Store
end
- Use
Cache<TKey, TValue>when you need automatic eviction, expiration, or access tracking. - Use
Dictionary<TKey, TValue>for simple lookups without size limits or expiration. - Use
ConcurrentDictionary<TKey, TValue>for thread-safe access without cache semantics.
| Policy | Description | Best For |
|---|---|---|
| LRU | Evicts least recently used entry | General purpose, most common |
| SLRU | Segmented LRU with probation/protected segments | High-throughput with scan resistance |
| LFU | Evicts least frequently used entry | Stable access patterns |
| FIFO | First in, first out eviction | Render caches, predictable eviction |
| Random | Random eviction | Low overhead, uniform access |
using WallstopStudios.UnityHelpers.Core.DataStructure;
// Simple LRU cache with fluent builder
Cache<string, UserData> cache = CacheBuilder<string, UserData>.NewBuilder()
.MaximumSize(1000)
.ExpireAfterWrite(TimeSpan.FromMinutes(5))
.EvictionPolicy(EvictionPolicy.Lru)
.Build();
// Basic operations
cache.Set("user1", userData);
if (cache.TryGet("user1", out UserData data))
{
// Use cached data
}
cache.TryRemove("user1");
cache.Clear();
cache.Resize(256); // evicts least-recently-used entries immediately when shrinking
cache.Resize(0); // removes the entry-count bound while retaining dynamic allocationusing WallstopStudios.UnityHelpers.Core.DataStructure;
// Loading cache - auto-computes missing values
Cache<int, ExpensiveResult> loadingCache = CacheBuilder<int, ExpensiveResult>.NewBuilder()
.MaximumSize(100)
.Build(key => ComputeExpensiveResult(key));
// GetOrAdd uses the loader when key is missing
ExpensiveResult result = loadingCache.GetOrAdd(42, null);using WallstopStudios.UnityHelpers.Core.DataStructure;
public sealed class PathResult { }
Cache<string, PathResult> pathCache = CacheBuilder<string, PathResult>.NewBuilder()
.MaximumSize(2000)
.InitialCapacity(64) // Start small, grow as needed
.ExpireAfterWrite(300f) // 5 minutes TTL
.WithJitter(12f) // Prevent thundering herd
.EvictionPolicy(EvictionPolicy.Slru) // Scan-resistant eviction
.ProtectedRatio(0.8f) // 80% protected segment for SLRU
.AllowGrowth(1.5f, 4000) // Grow 1.5x up to 4000 when thrashing
.RecordStatistics() // Enable hit/miss tracking
.OnEviction((key, value, reason) => Debug.Log($"Evicted {key}: {reason}"))
.OnGet((key, value) => Debug.Log($"Cache hit: {key}"))
.OnSet((key, value) => Debug.Log($"Cache set: {key}"))
.Build();
// Access statistics
CacheStatistics stats = pathCache.GetStatistics();
Debug.Log($"Hit rate: {stats.HitRate:P1}, Evictions: {stats.EvictionCount}");using WallstopStudios.UnityHelpers.Core.DataStructure;
// Weight-based eviction (e.g., by byte size)
Cache<string, Texture2D> textureCache = CacheBuilder<string, Texture2D>.NewBuilder()
.MaximumWeight(100_000_000) // 100 MB total
.Weigher((key, tex) => tex.width * tex.height * 4) // Bytes per texture
.ExpireAfterAccess(TimeSpan.FromMinutes(10)) // Sliding window
.Build();Use CachePresets for common scenarios:
using WallstopStudios.UnityHelpers.Core.DataStructure;
// Short-lived cache: 100 entries, 60s TTL, LRU
Cache<int, Vector3> tempCache = CachePresets.ShortLived<int, Vector3>().Build();
// Long-lived cache: 1000 entries, no TTL, LRU
Cache<string, GameObject> prefabCache = CachePresets.LongLived<string, GameObject>()
.Build(key => Resources.Load<GameObject>($"Prefabs/{key}"));
// Session cache: 500 entries, 30 min sliding window, LRU
Cache<string, InventoryData> sessionCache = CachePresets.SessionCache<string, InventoryData>().Build();
// High-throughput: 2000 entries, 5 min TTL, SLRU, growth enabled
Cache<(Vector3, Vector3), NavMeshPath> pathCache = CachePresets.HighThroughput<(Vector3, Vector3), NavMeshPath>().Build();
// Render cache: 200 entries, 30s TTL, FIFO
Cache<int, MaterialPropertyBlock> renderCache = CachePresets.RenderCache<int, MaterialPropertyBlock>().Build();
// Network cache: 100 entries, 2 min TTL with jitter, LRU
Cache<string, JsonResponse> apiCache = CachePresets.NetworkCache<string, JsonResponse>().Build();| Member | Description |
|---|---|
Count |
Current number of entries |
Size |
Total weight (weighted caching) or count |
MaximumSize |
Configured maximum entries |
Capacity |
Current internal capacity (may be < MaximumSize) |
Keys |
Enumerable of all keys (allocates state machine) |
TryGet(key, out value) |
Returns true if key exists and not expired |
Set(key, value) |
Adds or updates an entry |
GetOrAdd(key, factory) |
Gets existing or computes and caches new value (factory optional with loader) |
TryRemove(key) |
Removes entry and invokes OnEviction with Explicit by default |
TryRemove(key, out value) |
Removes entry, returns its value, and invokes OnEviction by default |
ContainsKey(key) |
Checks if key exists |
Clear() |
Removes all entries |
CleanUp() |
Forces expiration scan |
Compact(ratio) |
Evicts percentage of entries |
Resize(newSize) |
Changes the live count bound; zero removes it; weighted caches are unaffected |
GetStatistics() |
Returns hit/miss/eviction stats (if enabled) |
GetAll(keys, dict) |
Batch get into provided dictionary |
SetAll(items) |
Batch set from collection |
GetKeys(list) |
Zero-allocation key enumeration into provided list |
-
Choose the right preset:
CachePresetsprovides optimized defaults for common scenarios. - Enable statistics sparingly: Recording stats adds overhead; enable only when debugging.
- Use jitter for network caches: Prevents thundering herd when many entries expire together.
- Consider SLRU for high-throughput: Better scan resistance than plain LRU.
- Watch InitialCapacity: The cache clamps initial capacity to prevent OutOfMemoryException. Don't set it larger than needed.
-
Weighted caches: Use
MaximumWeight+Weigherfor size-based eviction (e.g., texture bytes). The budget applies to inserts and replacements even with adaptive growth enabled. Weights must be nonnegative; entries heavier than the whole budget are rejected without releasing the existing value or invokingOnSet. A throwing weigher propagates its exception before changing values, expiry, storage, or ownership. Zero-weight entries remain valid, and entry storage grows independently ofMaximumSizeso they are retained. -
Loader ownership on failed admission: If
WeigherorExpireAfterthrows duringGetOrAdd, the cache reports a factory result that no entry retains toOnEvictionasReplacedafter releasing its lock, then propagates the original exception. A reference still held by another entry stays owned by that entry. A rejected value supplied throughSetremains the caller's responsibility. ConfigureOnEvictionto release disposable loader resources. -
Replacement ownership follows the declared value type: Value types, including nullable value types, compare by value. Reference types compare by identity. Equal boxed numbers stored as
objectremain separate owned objects. -
Callbacks are synchronous:
OnEviction,OnGet,OnSetrun on the calling thread. Eviction callbacks run after the cache lock is released.Setinvokes one only for a different replaced value; capacity eviction andClearinvoke one per released value. By defaultTryRemoveinvokesOnEvictionwithExplicit; add.TransferOwnershipOnRemoval()when removal hands the returned value to the caller and must not release it.