Skip to content

Modifying a Contiguous*/OrderedMap/InsertionOrderMap during foreach silently skips elements instead of throwing #70

Description

@matt-edmondson

What's wrong

Several array-backed containers enumerate with a bare for (int i = 0; i < Count; i++) yield return items[i]; and keep no version counter. Changing the collection mid-enumeration therefore never throws InvalidOperationException, which the IEnumerator<T> contract expects. Instead, elements are silently skipped or visited twice.

Affected enumerators:

  • ContiguousCollection.GetEnumerator (ContiguousCollection.cs ~342)
  • ContiguousSet.GetEnumerator (~325)
  • ContiguousMap.GetEnumerator (~506), and its Keys/Values collections (~683, ~737)
  • OrderedMap (~399, ~499, ~551)
  • InsertionOrderMap (~383, ~441, ~486): these index items[i] directly, so the inner List<T> never gets the chance to throw
  • RingBuffer (~284)

The List<T>-backed siblings (InsertionOrderCollection, InsertionOrderSet, OrderedCollection, OrderedSet) do throw, so behaviour is inconsistent across the library.

Repro

var c = new ContiguousCollection<int> { 1, 2, 3, 4 };
foreach (var x in c) c.Remove(x);
  • Actual: no exception, and c still contains 2, 4.
  • Expected: InvalidOperationException, which is what InsertionOrderCollection throws for the same code.

ContiguousSet, ContiguousMap, OrderedMap and InsertionOrderMap behave the same way (for the maps, remove kv.Key). I confirmed all five in a scratch console app against the current main. This is the underlying cause of the #65 ExceptWith(self) symptom, which was fixed locally rather than at the enumerator.

Why it matters

A "remove while iterating" mistake produces wrong data with no error, and only for some container types. Code that works (by throwing) on one container gives quietly wrong results after switching to its faster contiguous sibling.

Suggested fix / acceptance criteria

  • Add a private int version to each affected type and increment it on every structural or value change: Add, Insert, Remove, RemoveAt, Clear, the indexer set, and for RingBuffer PushBack/Resize/Resample.
  • Each enumerator, including the map Keys/Values views, captures the version at its start and throws InvalidOperationException when it changes.
  • Add tests matching the existing List-backed behaviour for each type.
  • RingBuffer is commonly written to in real time, so adding the check there is a behaviour change; keep it out of scope if that is preferred.

Activity

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions