Skip to content

On netstandard builds, ContiguousMap (and ContiguousCollection/Set of reference-holding structs) never release removed or cleared items #79

Description

@matt-edmondson

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions