diff --git a/DeepClone.Test/SpecializedCollectionTests.cs b/DeepClone.Test/SpecializedCollectionTests.cs index a0db437..6679852 100644 --- a/DeepClone.Test/SpecializedCollectionTests.cs +++ b/DeepClone.Test/SpecializedCollectionTests.cs @@ -95,6 +95,93 @@ public void ReadOnlyDictionary_DeepClone_Null_ShouldThrow() Assert.ThrowsExactly(() => original.DeepClone()); } + /// + /// Tests that deep cloning a dictionary through IDictionary keeps its key comparer. + /// + [TestMethod] + public void Dictionary_DeepCloneAsIDictionary_KeepsComparer() + { + Dictionary original = new(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 }; + + IDictionary clone = ((IDictionary)original).DeepClone(); + + Assert.IsTrue(clone.ContainsKey("key"), "A case-insensitive clone should find a key in any case"); + Assert.AreSame(StringComparer.OrdinalIgnoreCase, ((Dictionary)clone).Comparer); + } + + /// + /// Tests that deep cloning a dictionary through IReadOnlyDictionary keeps its key comparer. + /// + [TestMethod] + public void Dictionary_DeepCloneAsIReadOnlyDictionary_KeepsComparer() + { + Dictionary original = new(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 }; + + IReadOnlyDictionary clone = ((IReadOnlyDictionary)original).DeepClone(); + + Assert.IsTrue(clone.ContainsKey("key"), "A case-insensitive clone should find a key in any case"); + } + + /// + /// Tests that deep cloning a sorted dictionary through IDictionary returns a sorted dictionary with the same comparer. + /// + [TestMethod] + public void SortedDictionary_DeepCloneAsIDictionary_KeepsTypeAndComparer() + { + IComparer descending = Comparer.Create((x, y) => string.CompareOrdinal(y, x)); + SortedDictionary original = new(descending) { ["a"] = 1, ["c"] = 3, ["b"] = 2 }; + + IDictionary clone = ((IDictionary)original).DeepClone(); + + SortedDictionary sortedClone = Assert.IsInstanceOfType>(clone); + Assert.AreSame(descending, sortedClone.Comparer); + Assert.AreEqual("c,b,a", string.Join(",", sortedClone.Keys)); + } + + /// + /// Tests that a Dictionary can be deep cloned without a cast, and that the clone keeps its comparer. + /// + [TestMethod] + public void Dictionary_DeepClone_WithoutCast_KeepsComparerAndClonesValues() + { + Dictionary original = new(StringComparer.OrdinalIgnoreCase) + { + ["Key"] = new() { Id = 1, Name = "Item1" }, + }; + + Dictionary clone = original.DeepClone(); + + Assert.AreSame(original.Comparer, clone.Comparer); + Assert.AreEqual(1, clone["key"].Id); + Assert.AreNotSame(original["Key"], clone["Key"]); + } + + /// + /// Tests that a SortedDictionary can be deep cloned without a cast, and that the clone keeps its comparer. + /// + [TestMethod] + public void SortedDictionary_DeepClone_WithoutCast_KeepsComparer() + { + IComparer descending = Comparer.Create((x, y) => string.CompareOrdinal(y, x)); + SortedDictionary original = new(descending) { ["a"] = 1, ["c"] = 3, ["b"] = 2 }; + + SortedDictionary clone = original.DeepClone(); + + Assert.AreSame(descending, clone.Comparer); + Assert.AreEqual("c,b,a", string.Join(",", clone.Keys)); + } + + /// + /// Tests that deep cloning a null Dictionary throws. + /// + [TestMethod] + public void Dictionary_DeepClone_Null_ShouldThrow() + { + Dictionary original = null!; + + Assert.ThrowsExactly(() => original.DeepClone()); + } + /// /// Tests deep cloning a SortedSet. /// diff --git a/DeepClone/DeepCloneContainerExtensions.cs b/DeepClone/DeepCloneContainerExtensions.cs index b69f53f..11d5f80 100644 --- a/DeepClone/DeepCloneContainerExtensions.cs +++ b/DeepClone/DeepCloneContainerExtensions.cs @@ -125,8 +125,10 @@ public static IEnumerable DeepClone(this IEnumerable source) => /// A new dictionary containing deep clones of the keys and values if they implement IDeepCloneable, /// otherwise containing the original keys and values. /// - /// This method returns a new Dictionary with cloned key-value pairs. Both keys and values - /// are deep cloned if they implement IDeepCloneable. + /// This method returns a new dictionary with cloned key-value pairs. Both keys and values + /// are deep cloned if they implement IDeepCloneable. A + /// is cloned as a sorted dictionary, and the source's key comparer is kept when the runtime type + /// exposes one ( or ). /// /// Example usage: /// @@ -139,9 +141,50 @@ public static IDictionary DeepClone(this IDictionary { Ensure.NotNull(source); - return source.ToDictionary( - pair => DeepClone(pair.Key), - pair => DeepClone(pair.Value)); + return CloneDictionary(source, source); + } + + /// + /// Deep clones a dictionary, keeping its key comparer. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// A new dictionary 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 + /// dictionary.DeepClone() compile without a cast. + /// + /// Thrown if source is null. + public static Dictionary DeepClone(this Dictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + Dictionary clone = new(source.Count, source.Comparer); + AddClonedPairs(clone, source); + return clone; + } + + /// + /// Deep clones a sorted dictionary, keeping its key comparer. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The source dictionary to clone. + /// A new sorted dictionary with the source's comparer, containing deep clones of the keys and values + /// if they implement IDeepCloneable, otherwise containing the original keys and values. + /// Thrown if source is null. + public static SortedDictionary DeepClone(this SortedDictionary source) + where TKey : notnull + { + Ensure.NotNull(source); + + SortedDictionary clone = new(source.Comparer); + AddClonedPairs(clone, source); + return clone; } /// @@ -153,7 +196,9 @@ public static IDictionary DeepClone(this IDictionary /// A new read-only dictionary containing deep clones of the keys and values if they implement IDeepCloneable, /// otherwise containing the original keys and values. /// - /// This method returns a new read-only dictionary with cloned key-value pairs. + /// This method returns a new read-only dictionary with cloned key-value pairs. As with the + /// overload, a sorted dictionary stays sorted and the source's + /// key comparer is kept when the runtime type exposes one. /// /// Example usage: /// @@ -166,9 +211,46 @@ public static IReadOnlyDictionary DeepClone(this IRe { Ensure.NotNull(source); - return source.ToDictionary( - pair => DeepClone(pair.Key), - pair => DeepClone(pair.Value)); + return (IReadOnlyDictionary)CloneDictionary(source, source); + } + + /// + /// Creates an empty dictionary of the same kind, and with the same key comparer, as + /// where its runtime type exposes one, and fills it with deep clones of . + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The dictionary being cloned, inspected for its runtime type and comparer. + /// The key-value pairs of the dictionary being cloned. + /// A for a sorted source, otherwise a . + /// Both implement . + private static IDictionary CloneDictionary(object source, IEnumerable> pairs) + where TKey : notnull + { + IDictionary clone = source switch + { + SortedDictionary sorted => new SortedDictionary(sorted.Comparer), + Dictionary dictionary => new Dictionary(dictionary.Count, dictionary.Comparer), + _ => new Dictionary(), + }; + AddClonedPairs(clone, pairs); + return clone; + } + + /// + /// Adds a deep clone of each key-value pair to a destination dictionary. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The dictionary to add to. + /// The key-value pairs to clone. + private static void AddClonedPairs(IDictionary dest, IEnumerable> pairs) + where TKey : notnull + { + foreach (KeyValuePair pair in pairs) + { + dest.Add(DeepClone(pair.Key), DeepClone(pair.Value)); + } } ///