Skip to content

Keep the key comparer when deep cloning a dictionary - #79

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/77-dictionary-comparer
Sep 26, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/77-dictionary-comparer

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #77

What was wrong

The IDictionary and IReadOnlyDictionary overloads of DeepClone() used ToDictionary, which always builds a Dictionary with the default comparer:

  • A cloned OrdinalIgnoreCase dictionary no longer found "key".
  • A SortedDictionary came back as an unordered Dictionary.

Separately, dict.DeepClone() on a Dictionary<,> didn't compile (CS0121), because it matches both interface overloads equally well.

Change

  • Interface overloads: these now go through a shared CloneDictionary helper.
    • If the runtime type is SortedDictionary<,>, it builds a SortedDictionary with the same comparer.
    • If it is Dictionary<,>, it builds a Dictionary with the same comparer.
    • Anything else still gets a plain Dictionary, as before.
  • New concrete overloads: Dictionary<TKey,TValue> DeepClone(this Dictionary<TKey,TValue>) and SortedDictionary<TKey,TValue> DeepClone(this SortedDictionary<TKey,TValue>).
    • Both keep the comparer.
    • They are the most specific match, so the CS0121 ambiguity goes away for both types, which the triage asked to check.
  • The commit carries [minor] because it adds public API. Existing call sites that cast to an interface behave the same apart from keeping the comparer.

Tests

New tests in SpecializedCollectionTests:

  • Dictionary_DeepCloneAsIDictionary_KeepsComparer and Dictionary_DeepCloneAsIReadOnlyDictionary_KeepsComparer: a case-insensitive lookup still works after cloning.
  • SortedDictionary_DeepCloneAsIDictionary_KeepsTypeAndComparer: the clone is still sorted, with a custom descending comparer.
  • Dictionary_DeepClone_WithoutCast_KeepsComparerAndClonesValues, SortedDictionary_DeepClone_WithoutCast_KeepsComparer and Dictionary_DeepClone_Null_ShouldThrow: the concrete overloads.

Verification

  • Against main, the three interface tests fail (ContainsKey("key") is false, and the clone's type is Dictionary).
  • Against main, the three concrete-call tests don't compile, with CS0121 between the two interface overloads.
  • With the fix, 42 of 42 tests pass, and the Release build, netstandard2.0 included, has no warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NvZoJBSka1emZMaEXy4Cfj


Generated by Claude Code

The IDictionary and IReadOnlyDictionary overloads always built a Dictionary
with the default comparer. A cloned case-insensitive dictionary stopped finding
keys, and a SortedDictionary came back unordered. They now build a dictionary
of the same kind, with the same comparer, when the runtime type is a
Dictionary or a SortedDictionary.

New concrete Dictionary and SortedDictionary overloads keep the comparer too.
Because both types implement IDictionary and IReadOnlyDictionary, these
overloads also let dictionary.DeepClone() compile without a cast (CS0121).

Fixes #77

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvZoJBSka1emZMaEXy4Cfj
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dictionary DeepClone() drops the source's key comparer, so a cloned case-insensitive dictionary stops finding keys

2 participants