What's wrong
Both sets keep a HashSet<T> (uniquenessSet) built with the caller's IEqualityComparer<T>, next to an ordered backing store. Remove uses the comparer for the hash set but searches the backing store with default equality:
Containers/ContiguousSet.cs, Remove: int index = Array.IndexOf(items, item, 0, Count);
Containers/InsertionOrderSet.cs, Remove: items.Remove(item); (List<T>.Remove, default equality)
When the argument is equal under the comparer but not under Equals, the hash-set entry is removed and the backing-store entry is not. The two structures then disagree.
Reproduction (confirmed with a temporary MSTest against current main)
ContiguousSet<string> set = new(StringComparer.OrdinalIgnoreCase) { "Apple" };
bool removed = set.Remove("APPLE");
// removed == true, Count == 1, Contains("apple") == false, enumeration yields ["Apple"]
set.Add("apple");
// Count == 2, enumeration yields ["Apple", "apple"]: a duplicate in a set
InsertionOrderSet<string> gives exactly the same output.
Why it matters
A case-insensitive set of names, paths or tags is the main reason to pass a comparer. On such a set, one Remove with different casing breaks the set:
Remove reports success, but the item still enumerates.
Count stays the same.
Contains says the item is absent.
- A later
Add stores a second copy, which breaks the set's uniqueness invariant.
Suggested fix
Find the index with the set's own comparer rather than default equality:
// ContiguousSet
IEqualityComparer<T> cmp = uniquenessSet.Comparer;
int index = -1;
for (int i = 0; i < Count; i++) { if (cmp.Equals(items[i], item)) { index = i; break; } }
// InsertionOrderSet
int index = items.FindIndex(x => uniquenessSet.Comparer.Equals(x, item));
items.RemoveAt(index);
(#51 fixed the same kind of comparer bypass in OrderedSet<T>.)
Acceptance criteria
- With
StringComparer.OrdinalIgnoreCase, Remove("APPLE") on a set holding "Apple" leaves Count == 0 and nothing to enumerate, for both types.
- A following
Add("apple") gives exactly one element.
- Regression tests cover both types.
What's wrong
Both sets keep a
HashSet<T>(uniquenessSet) built with the caller'sIEqualityComparer<T>, next to an ordered backing store.Removeuses the comparer for the hash set but searches the backing store with default equality:Containers/ContiguousSet.cs,Remove:int index = Array.IndexOf(items, item, 0, Count);Containers/InsertionOrderSet.cs,Remove:items.Remove(item);(List<T>.Remove, default equality)When the argument is equal under the comparer but not under
Equals, the hash-set entry is removed and the backing-store entry is not. The two structures then disagree.Reproduction (confirmed with a temporary MSTest against current
main)InsertionOrderSet<string>gives exactly the same output.Why it matters
A case-insensitive set of names, paths or tags is the main reason to pass a comparer. On such a set, one
Removewith different casing breaks the set:Removereports success, but the item still enumerates.Countstays the same.Containssays the item is absent.Addstores a second copy, which breaks the set's uniqueness invariant.Suggested fix
Find the index with the set's own comparer rather than default equality:
(#51 fixed the same kind of comparer bypass in
OrderedSet<T>.)Acceptance criteria
StringComparer.OrdinalIgnoreCase,Remove("APPLE")on a set holding"Apple"leavesCount == 0and nothing to enumerate, for both types.Add("apple")gives exactly one element.