From f75f2478873ad45176f714153ec38618c740d4b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 00:28:24 +0000 Subject: [PATCH] [patch] Let SortedList and the immutable dictionaries call DeepClone() directly SortedList, ImmutableDictionary and ImmutableSortedDictionary implement both IDictionary and IReadOnlyDictionary, so calling .DeepClone() on them failed with CS0121. Add concrete overloads, as #81 did for ConcurrentDictionary and ReadOnlyDictionary. Each returns the source's type and keeps its comparers. Fixes ktsu-dev/DeepClone#86 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSUYMQMykgfsb5y6AhnKb --- .../DirectDictionaryOverloadTests.cs | 81 +++++++++++++++++++ DeepClone/DeepCloneContainerExtensions.cs | 80 ++++++++++++++++++ README.md | 5 +- 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 DeepClone.Test/DirectDictionaryOverloadTests.cs diff --git a/DeepClone.Test/DirectDictionaryOverloadTests.cs b/DeepClone.Test/DirectDictionaryOverloadTests.cs new file mode 100644 index 0000000..e8c22a7 --- /dev/null +++ b/DeepClone.Test/DirectDictionaryOverloadTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.DeepClone.Test; + +using System.Collections.Immutable; + +/// +/// Tests that .DeepClone() can be called directly on dictionary types that implement both +/// and , and that the +/// result has the source's type and comparer (ktsu-dev/DeepClone#86). Each test calls the extension without +/// a cast, so this file stops compiling if the overloads become ambiguous again. +/// +[TestClass] +public class DirectDictionaryOverloadTests +{ + private static readonly string[] DescendingKeys = ["c", "b", "a"]; + + /// + /// Tests that a sorted list clones to a sorted list with the source's comparer and deep-cloned values. + /// + [TestMethod] + public void SortedList_DeepClone_KeepsTypeComparerAndClonesValues() + { + Comparer descending = Comparer.Create((x, y) => string.CompareOrdinal(y, x)); + SortedList original = new(descending) + { + ["a"] = new SimpleObject { Id = 1, Name = "A" }, + ["c"] = new SimpleObject { Id = 3, Name = "C" }, + }; + + SortedList clone = original.DeepClone(); + clone["b"] = new SimpleObject { Id = 2, Name = "B" }; + + Assert.AreSame(descending, clone.Comparer); + Assert.AreSequenceEqual(DescendingKeys, clone.Keys); + Assert.AreNotSame(original["a"], clone["a"]); + Assert.AreEqual(1, clone["a"].Id); + } + + /// + /// Tests that an immutable dictionary clones to an immutable dictionary with the source's key and value + /// comparers and deep-cloned values. + /// + [TestMethod] + public void ImmutableDictionary_DeepClone_KeepsTypeComparersAndClonesValues() + { + ImmutableDictionary original = ImmutableDictionary + .Create(StringComparer.OrdinalIgnoreCase, ReferenceEqualityComparer.Instance) + .Add("Key", new SimpleObject { Id = 1, Name = "One" }); + + ImmutableDictionary clone = original.DeepClone(); + + Assert.AreSame(StringComparer.OrdinalIgnoreCase, clone.KeyComparer); + Assert.AreSame(ReferenceEqualityComparer.Instance, clone.ValueComparer); + Assert.IsTrue(clone.ContainsKey("KEY")); + Assert.AreNotSame(original["Key"], clone["Key"]); + Assert.AreEqual(1, clone["Key"].Id); + } + + /// + /// Tests that an immutable sorted dictionary clones to an immutable sorted dictionary with the source's + /// key and value comparers and deep-cloned values. + /// + [TestMethod] + public void ImmutableSortedDictionary_DeepClone_KeepsTypeComparersAndClonesValues() + { + Comparer descending = Comparer.Create((x, y) => string.CompareOrdinal(y, x)); + ImmutableSortedDictionary original = ImmutableSortedDictionary + .Create(descending, ReferenceEqualityComparer.Instance) + .Add("a", new SimpleObject { Id = 1, Name = "A" }) + .Add("c", new SimpleObject { Id = 3, Name = "C" }); + + ImmutableSortedDictionary clone = original.DeepClone().Add("b", new SimpleObject { Id = 2, Name = "B" }); + + Assert.AreSame(descending, clone.KeyComparer); + Assert.AreSame(ReferenceEqualityComparer.Instance, clone.ValueComparer); + Assert.AreSequenceEqual(DescendingKeys, clone.Keys); + Assert.AreNotSame(original["a"], clone["a"]); + Assert.AreEqual(1, clone["a"].Id); + } +} diff --git a/DeepClone/DeepCloneContainerExtensions.cs b/DeepClone/DeepCloneContainerExtensions.cs index 1f677e8..09b84aa 100644 --- a/DeepClone/DeepCloneContainerExtensions.cs +++ b/DeepClone/DeepCloneContainerExtensions.cs @@ -255,6 +255,86 @@ public static SortedDictionary DeepClone(this Sorted return clone; } + /// + /// Deep clones a sorted list, keeping its key comparer. + /// + /// The type of keys in the list. + /// The type of values in the list. + /// The source list to clone. + /// A new sorted list with the source's comparer, containing deep clones of the keys and values + /// if they implement IDeepCloneable, otherwise containing the original keys and values. + /// + /// implements both and + /// , so this overload is also what lets + /// sortedList.DeepClone() compile without a cast. + /// + /// Thrown if source is null. + public static SortedList DeepClone(this SortedList source) + where TKey : notnull + { + Ensure.NotNull(source); + + SortedList clone = new(source.Count, source.Comparer); + AddClonedPairs(clone, source); + return clone; + } + +#if NET + /// + /// Deep clones an immutable dictionary, keeping its key and value comparers. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// A new immutable dictionary with the source's comparers, containing deep clones of the keys and values + /// if they implement IDeepCloneable, otherwise containing the original keys and values. + /// + /// implements both and + /// , so this overload is also what lets + /// immutableDictionary.DeepClone() compile without a cast. + /// + /// Thrown if source is null. + public static ImmutableDictionary DeepClone(this ImmutableDictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + return ImmutableDictionary.CreateRange(source.KeyComparer, source.ValueComparer, ClonePairs(source)); + } + + /// + /// Deep clones an immutable sorted dictionary, keeping its key and value comparers. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// A new immutable sorted dictionary with the source's comparers, containing deep clones of the keys and + /// values if they implement IDeepCloneable, otherwise containing the original keys and values. + /// + /// implements both + /// and , so this overload is also what lets + /// immutableSortedDictionary.DeepClone() compile without a cast. + /// + /// Thrown if source is null. + public static ImmutableSortedDictionary DeepClone(this ImmutableSortedDictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + return ImmutableSortedDictionary.CreateRange(source.KeyComparer, source.ValueComparer, ClonePairs(source)); + } + + /// + /// Deep clones the key and value of each pair. + /// + /// The type of the keys. + /// The type of the values. + /// The key-value pairs to clone. + /// The cloned pairs. + private static IEnumerable> ClonePairs(IEnumerable> pairs) => + pairs.Select(p => new KeyValuePair(DeepClone(p.Key), DeepClone(p.Value))); +#endif + /// /// Deep clones a concurrent dictionary, keeping its key comparer where the target framework exposes it. /// diff --git a/README.md b/README.md index 9feccba..0c197b2 100644 --- a/README.md +++ b/README.md @@ -603,13 +603,14 @@ Extension methods for deep cloning various container types. | `DeepClone` | `IEnumerable?` | Creates a deep clone of an enumerable collection | | `DeepClone` | `IDictionary?` | Creates a deep clone of a dictionary, preserving the key comparer where the runtime type exposes one | | `DeepClone` | `IReadOnlyDictionary?` | Creates a deep clone of a read-only dictionary, preserving the key comparer where the runtime type exposes one | +| `DeepClone` | `SortedList` | Creates a deep clone of a sorted list, preserving the key comparer | | `DeepClone` | `HashSet?` | Creates a deep clone of a hash set, preserving the comparer | | `DeepClone` | `SortedSet?` | Creates a deep clone of a sorted set, preserving the comparer | | `DeepClone` | `Stack?` | Creates a deep clone of a stack, preserving the element order | | `DeepClone` | `ImmutableArray` | Creates a deep clone of an immutable array | | `DeepClone` | `ImmutableList` | Creates a deep clone of an immutable list | -| `DeepClone` | `ImmutableDictionary` | Creates a deep clone of an immutable dictionary | -| `DeepClone` | `ImmutableSortedDictionary` | Creates a deep clone of an immutable sorted dictionary | +| `DeepClone` | `ImmutableDictionary` | Creates a deep clone of an immutable dictionary, preserving the key and value comparers (.NET only) | +| `DeepClone` | `ImmutableSortedDictionary` | Creates a deep clone of an immutable sorted dictionary, preserving the key and value comparers (.NET only) | | `DeepClone` | `ImmutableHashSet` | Creates a deep clone of an immutable hash set | | `DeepClone` | `ImmutableSortedSet` | Creates a deep clone of an immutable sorted set | | `DeepClone` | `ImmutableStack` | Creates a deep clone of an immutable stack |