What's wrong
ContiguousSet<T>.AsSpan() (Containers/ContiguousSet.cs:543) and ContiguousMap<TKey,TValue>.AsSpan() (Containers/ContiguousMap.cs:565) return a mutable Span<T> / Span<Entry> over the backing items array:
public Span<T> AsSpan() => new(items, 0, Count); // ContiguousSet
public Span<Entry> AsSpan() => new(items, 0, Count); // ContiguousMap
Both types also keep a side index: uniquenessSet for the set and keyToIndex for the map. Writing through the span changes items but leaves that index untouched, so the container breaks its own invariants with no error. The doc comments give no warning. ContiguousCollection<T>.AsSpan() is fine because it has no side index.
Reproduction (verified against a local build of main)
Set
var s = new ContiguousSet<int> { 1, 2, 3 };
s.AsSpan()[0] = 2;
// items enumerate as 2,2,3 -> duplicate inside a set
s.Contains(1); // True, although 1 is no longer stored
s.Add(1); // False, so 1 cannot be re-added
s.Remove(2); // items now 2,3
s.Remove(2); // False, but 2 is still enumerated
Map
var m = new ContiguousMap<string,int> { ["a"] = 1, ["b"] = 2 };
m.AsSpan()[0] = new ContiguousMap<string,int>.Entry("b", 99);
// Keys enumerate as b,b
m["a"]; // 99
m.ContainsKey("a"); // True
Any caller who treats AsSpan() as the fast path for in-place updates, which is what a writable span invites, corrupts the container with no error.
Suggested fix
- Make
AsSpan() on ContiguousSet and ContiguousMap return ReadOnlySpan<…>, or remove it and keep only AsReadOnlySpan(). This is a breaking public API change, so it needs a major/minor version bump per the repo's versioning.
- If in-place value mutation on the map is a use case worth keeping, add a narrow API instead, e.g.
ref TValue GetValueRefOrNullRef(TKey key) in the style of CollectionsMarshal. It can update values without letting callers rewrite keys.
Acceptance criteria: no public API on ContiguousSet or ContiguousMap can change stored elements or keys without updating uniquenessSet / keyToIndex, and a test covers the scenario above.
What's wrong
ContiguousSet<T>.AsSpan()(Containers/ContiguousSet.cs:543) andContiguousMap<TKey,TValue>.AsSpan()(Containers/ContiguousMap.cs:565) return a mutableSpan<T>/Span<Entry>over the backingitemsarray:Both types also keep a side index:
uniquenessSetfor the set andkeyToIndexfor the map. Writing through the span changesitemsbut leaves that index untouched, so the container breaks its own invariants with no error. The doc comments give no warning.ContiguousCollection<T>.AsSpan()is fine because it has no side index.Reproduction (verified against a local build of
main)Set
Map
Any caller who treats
AsSpan()as the fast path for in-place updates, which is what a writable span invites, corrupts the container with no error.Suggested fix
AsSpan()onContiguousSetandContiguousMapreturnReadOnlySpan<…>, or remove it and keep onlyAsReadOnlySpan(). This is a breaking public API change, so it needs a major/minor version bump per the repo's versioning.ref TValue GetValueRefOrNullRef(TKey key)in the style ofCollectionsMarshal. It can update values without letting callers rewrite keys.Acceptance criteria: no public API on
ContiguousSetorContiguousMapcan change stored elements or keys without updatinguniquenessSet/keyToIndex, and a test covers the scenario above.