Skip to content

fix: make ContiguousSet/ContiguousMap.AsSpan read-only and add GetValueRefOrNullRef [major] - #109

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/78-readonly-span
Oct 7, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/78-readonly-span

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #78

What was wrong

ContiguousSet<T>.AsSpan() and ContiguousMap<TKey,TValue>.AsSpan() returned a writable Span<> over the backing array. A write through that span changed elements or keys but left uniquenessSet / keyToIndex alone. The set could then hold duplicates and report removed items as present, and the map could return a value for a key it no longer stored.

Change (as decided on the issue on 2026-09-28)

  • On both types, AsSpan() now returns ReadOnlySpan<…>.
  • New ContiguousMap.GetValueRefOrNullRef(TKey key), in the style of CollectionsMarshal.GetValueRefOrNullRef. It returns a writable ref TValue to the stored value, or a null ref when the key is missing (check with Unsafe.IsNullRef). Callers can update values in place but cannot touch keys. The null ref comes from MemoryMarshal.GetReference(Span<T>.Empty), so the netstandard targets need no extra dependency.
  • Entry keeps its value in an internal field so the accessor can return a ref to it. Its public members are unchanged: Key and Value stay get-only. The struct is no longer declared readonly struct, but each of its members is now marked readonly.

This breaks the public API (the return type of AsSpan()), so the title is tagged [major].

Tests

  • AsSpan_IsReadOnly_* on both types assert the return type. With the old Span<> return types restored, both fail and the other 444 tests pass.
  • GetValueRefOrNullRef_* tests cover updating a value in place, keeping the key index consistent (a case-insensitive key keeps its original spelling, and Remove still works), a null ref for a missing key, and a null key throwing. Without the change these tests do not compile.
  • Full suite: 446/446 pass on net10.0. The library builds clean on net10.0, net9.0, net8.0, netstandard2.1 and netstandard2.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J6q8NVYVFUeBVAH6tJCHnU


Generated by Claude Code

…ueRefOrNullRef [major]

AsSpan() on ContiguousSet<T> and ContiguousMap<TKey,TValue> returned a writable
span over the backing array, so writing through it changed elements or keys
without updating uniquenessSet / keyToIndex. A set could hold duplicates and
report removed items as present, and a map could answer lookups for a key it no
longer stored.

Both now return ReadOnlySpan<>. ContiguousMap gains GetValueRefOrNullRef(key),
in the style of CollectionsMarshal, so values can still be updated in place
without exposing keys. Entry stores its value in a field to make that possible;
its public surface is unchanged.

This is a breaking change to the return type of AsSpan().

Fixes #78

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6q8NVYVFUeBVAH6tJCHnU
Comment thread Containers/ContiguousMap.cs
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit be63448 into main Oct 7, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the fix/78-readonly-span branch October 7, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants