Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/api/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -3335,7 +3335,9 @@ frees.

LRU semantics require a lookup to count as a *use*. The indexer getter and `TryGet` therefore
**promote the entry to most-recently-used**, which reorders the recency list and invalidates any
in-progress enumerator (matching "collection was modified" semantics). To inspect the cache without
in-progress enumerator (matching "collection was modified" semantics). The one exception is a hit on
the entry that is *already* most-recently-used: there is nothing to reorder, so the promotion is a
no-op and active enumerators stay valid. To inspect the cache without
disturbing recency order — and without invalidating an active enumerator — use `TryPeek`,
`ContainsKey`, or the `TryPeekLeastRecentlyUsed` / `TryPeekMostRecentlyUsed` inspectors.

Expand Down
18 changes: 13 additions & 5 deletions src/Celerity/Collections/LruCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ namespace Celerity.Collections;
/// <para>
/// <b>Reads are mutating.</b> LRU semantics require a lookup to count as a use, so the indexer getter
/// and <see cref="TryGet(TKey, out TValue)"/> promote the entry to most-recently-used and therefore
/// invalidate any in-progress enumerator (matching "collection was modified" semantics). Use
/// invalidate any in-progress enumerator (matching "collection was modified" semantics) — except when
/// the entry is <i>already</i> the most-recently-used one, where the promotion is a no-op that leaves
/// the recency order and any active enumerator untouched. Use
/// <see cref="TryPeek(TKey, out TValue)"/> or <see cref="ContainsKey(TKey)"/> to inspect the cache
/// without disturbing recency order.
/// </para>
Expand Down Expand Up @@ -155,7 +157,10 @@ public TValue? this[TKey key]
/// value of <typeparamref name="TValue"/>.
/// </param>
/// <returns><c>true</c> if the key was found; otherwise <c>false</c>.</returns>
/// <remarks>A hit reorders the recency list and therefore invalidates active enumerators.</remarks>
/// <remarks>
/// A hit reorders the recency list and therefore invalidates active enumerators, unless the entry
/// is already the most-recently-used one — that promotion is a no-op and leaves them valid.
/// </remarks>
public bool TryGet(TKey key, out TValue? value)
{
if (!_index.TryGetValue(key, out int node))
Expand Down Expand Up @@ -346,9 +351,12 @@ public bool TryPeekMostRecentlyUsed(out TKey? key, out TValue? value)
/// <summary>
/// Returns an allocation-free struct enumerator that yields each entry in
/// <b>most-recently-used to least-recently-used</b> order. Enumeration is a peek: it does not
/// change recency. If the cache is modified during enumeration — including by a mutating read
/// (<see cref="TryGet(TKey, out TValue)"/> or the indexer getter) — <see cref="Enumerator.MoveNext"/>
/// throws <see cref="InvalidOperationException"/>.
/// change recency. <see cref="Enumerator.MoveNext"/> throws <see cref="InvalidOperationException"/>
/// when the entry set or the recency order changed since the enumerator was taken: an insert, an
/// eviction, a <see cref="Remove(TKey)"/>, a <see cref="Clear"/>, or any read or write that
/// promoted an entry which was not already most-recently-used. Overwriting the value of an entry
/// without moving it is not a structural change and leaves active enumerators valid, matching the
/// rest of the library.
/// </summary>
/// <returns>A struct enumerator over this cache.</returns>
public Enumerator GetEnumerator() => new Enumerator(this);
Expand Down
Loading