What's wrong
The code that clears slots on Remove/Clear only runs when this condition holds:
#if NET5_0_OR_GREATER
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
#else
if (!typeof(T).IsValueType)
#endif
On the netstandard2.0/2.1 targets, the fallback is false for every struct, including structs that hold references.
ContiguousMap.Entry is always a struct, so on the netstandard builds ContiguousMap<TKey, TValue> never clears a slot, whatever TKey/TValue are. See ContiguousMap.cs:376 (Remove) and :448 (Clear).
ContiguousCollection<T> (:171, :252) and ContiguousSet<T> (:228, :308) have the same problem for struct T that holds references, such as KeyValuePair<int, object> or (string, int).
Removed and cleared keys and values stay reachable from the backing array until their slot is overwritten.
Reproduction
The library was built for netstandard2.0 and consumed from a net10 app. Each case holds a WeakReference to a value, then runs GC.Collect() followed by GC.WaitForPendingFinalizers():
ContiguousMap<int, object> → Clear() → value still alive
ContiguousMap<int, object> → remove every key → value still alive
ContiguousCollection<KeyValuePair<int, object>> → Clear() → value still alive
The same program run against the net10 build reports every value as collected.
Why it matters
Consumers of the netstandard assemblies, such as .NET Framework, Unity and Mono, see memory leaks that net5+ users don't. A map used as a cache that is periodically cleared keeps every value it has ever held alive. This is the same class of bug as #72 (RingBuffer.Clear), but it affects different types and has a different root cause: the TFM guard.
Suggested fix
- Use
RuntimeHelpers.IsReferenceOrContainsReferences<T>() under #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER; the API exists in netstandard2.1.
- On netstandard2.0, drop the condition and always clear. Clearing is cheap and always correct.
- Apply the fix to every occurrence of the pattern (
grep -n "IsValueType" Containers/).
Acceptance criteria
- A test with a
WeakReference shows that values are collectable after Remove/Clear for ContiguousMap<int, object> and ContiguousCollection<KeyValuePair<int, object>>.
- The test runs on the netstandard-consuming leg if CI has one. Otherwise, verify it manually against the netstandard2.0 build.
What's wrong
The code that clears slots on Remove/Clear only runs when this condition holds:
On the netstandard2.0/2.1 targets, the fallback is
falsefor every struct, including structs that hold references.ContiguousMap.Entryis always a struct, so on the netstandard buildsContiguousMap<TKey, TValue>never clears a slot, whateverTKey/TValueare. SeeContiguousMap.cs:376(Remove) and:448(Clear).ContiguousCollection<T>(:171,:252) andContiguousSet<T>(:228,:308) have the same problem for structTthat holds references, such asKeyValuePair<int, object>or(string, int).Removed and cleared keys and values stay reachable from the backing array until their slot is overwritten.
Reproduction
The library was built for netstandard2.0 and consumed from a net10 app. Each case holds a
WeakReferenceto a value, then runsGC.Collect()followed byGC.WaitForPendingFinalizers():ContiguousMap<int, object>→Clear()→ value still aliveContiguousMap<int, object>→ remove every key → value still aliveContiguousCollection<KeyValuePair<int, object>>→Clear()→ value still aliveThe same program run against the net10 build reports every value as collected.
Why it matters
Consumers of the netstandard assemblies, such as .NET Framework, Unity and Mono, see memory leaks that net5+ users don't. A map used as a cache that is periodically cleared keeps every value it has ever held alive. This is the same class of bug as #72 (RingBuffer.Clear), but it affects different types and has a different root cause: the TFM guard.
Suggested fix
RuntimeHelpers.IsReferenceOrContainsReferences<T>()under#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER; the API exists in netstandard2.1.grep -n "IsValueType" Containers/).Acceptance criteria
WeakReferenceshows that values are collectable after Remove/Clear forContiguousMap<int, object>andContiguousCollection<KeyValuePair<int, object>>.