Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions DeepClone.Test/SpecializedCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,93 @@ public void ReadOnlyDictionary_DeepClone_Null_ShouldThrow()
Assert.ThrowsExactly<ArgumentNullException>(() => original.DeepClone());
}

/// <summary>
/// Tests that deep cloning a dictionary through IDictionary keeps its key comparer.
/// </summary>
[TestMethod]
public void Dictionary_DeepCloneAsIDictionary_KeepsComparer()
{
Dictionary<string, int> original = new(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 };

IDictionary<string, int> clone = ((IDictionary<string, int>)original).DeepClone();

Assert.IsTrue(clone.ContainsKey("key"), "A case-insensitive clone should find a key in any case");
Assert.AreSame(StringComparer.OrdinalIgnoreCase, ((Dictionary<string, int>)clone).Comparer);
}

/// <summary>
/// Tests that deep cloning a dictionary through IReadOnlyDictionary keeps its key comparer.
/// </summary>
[TestMethod]
public void Dictionary_DeepCloneAsIReadOnlyDictionary_KeepsComparer()
{
Dictionary<string, int> original = new(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 };

IReadOnlyDictionary<string, int> clone = ((IReadOnlyDictionary<string, int>)original).DeepClone();

Assert.IsTrue(clone.ContainsKey("key"), "A case-insensitive clone should find a key in any case");
}

/// <summary>
/// Tests that deep cloning a sorted dictionary through IDictionary returns a sorted dictionary with the same comparer.
/// </summary>
[TestMethod]
public void SortedDictionary_DeepCloneAsIDictionary_KeepsTypeAndComparer()
{
IComparer<string> descending = Comparer<string>.Create((x, y) => string.CompareOrdinal(y, x));
SortedDictionary<string, int> original = new(descending) { ["a"] = 1, ["c"] = 3, ["b"] = 2 };

IDictionary<string, int> clone = ((IDictionary<string, int>)original).DeepClone();

SortedDictionary<string, int> sortedClone = Assert.IsInstanceOfType<SortedDictionary<string, int>>(clone);
Assert.AreSame(descending, sortedClone.Comparer);
Assert.AreEqual("c,b,a", string.Join(",", sortedClone.Keys));
}

/// <summary>
/// Tests that a Dictionary can be deep cloned without a cast, and that the clone keeps its comparer.
/// </summary>
[TestMethod]
public void Dictionary_DeepClone_WithoutCast_KeepsComparerAndClonesValues()
{
Dictionary<string, SimpleObject> original = new(StringComparer.OrdinalIgnoreCase)
{
["Key"] = new() { Id = 1, Name = "Item1" },
};

Dictionary<string, SimpleObject> clone = original.DeepClone();

Assert.AreSame(original.Comparer, clone.Comparer);
Assert.AreEqual(1, clone["key"].Id);
Assert.AreNotSame(original["Key"], clone["Key"]);
}

/// <summary>
/// Tests that a SortedDictionary can be deep cloned without a cast, and that the clone keeps its comparer.
/// </summary>
[TestMethod]
public void SortedDictionary_DeepClone_WithoutCast_KeepsComparer()
{
IComparer<string> descending = Comparer<string>.Create((x, y) => string.CompareOrdinal(y, x));
SortedDictionary<string, int> original = new(descending) { ["a"] = 1, ["c"] = 3, ["b"] = 2 };

SortedDictionary<string, int> clone = original.DeepClone();

Assert.AreSame(descending, clone.Comparer);
Assert.AreEqual("c,b,a", string.Join(",", clone.Keys));
}

/// <summary>
/// Tests that deep cloning a null Dictionary throws.
/// </summary>
[TestMethod]
public void Dictionary_DeepClone_Null_ShouldThrow()
{
Dictionary<string, int> original = null!;

Assert.ThrowsExactly<ArgumentNullException>(() => original.DeepClone());
}

/// <summary>
/// Tests deep cloning a SortedSet.
/// </summary>
Expand Down
100 changes: 91 additions & 9 deletions DeepClone/DeepCloneContainerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
/// of individual elements. It checks if the object implements IDeepCloneable and calls
/// DeepClone() if it does, otherwise it returns the original object.
/// </remarks>
private static T DeepClone<T>(T source) => source == null ? default! : source is IDeepCloneable cloneable ? (T)cloneable.DeepClone() : source;

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type.

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type.

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 97 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Deep clones a collection of objects.
Expand All @@ -113,7 +113,7 @@
/// var clonedArray = originalArray.DeepClone().ToArray();
/// </code>
/// </remarks>
public static IEnumerable<T> DeepClone<T>(this IEnumerable<T> source) =>

Check warning on line 116 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 116 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 116 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 116 in DeepClone/DeepCloneContainerExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

All 'DeepClone' method overloads should be adjacent.

Check warning on line 116 in DeepClone/DeepCloneContainerExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

All 'DeepClone' method overloads should be adjacent.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_DeepClone&issues=AaDd8P0FxjvAjhH7jEFm&open=AaDd8P0FxjvAjhH7jEFm&pullRequest=79
source.Select(DeepClone);

/// <summary>
Expand All @@ -125,8 +125,10 @@
/// <returns>A new dictionary containing deep clones of the keys and values if they implement IDeepCloneable,
/// otherwise containing the original keys and values.</returns>
/// <remarks>
/// 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 <see cref="SortedDictionary{TKey, TValue}"/>
/// is cloned as a sorted dictionary, and the source's key comparer is kept when the runtime type
/// exposes one (<see cref="Dictionary{TKey, TValue}"/> or <see cref="SortedDictionary{TKey, TValue}"/>).
///
/// Example usage:
/// <code>
Expand All @@ -139,9 +141,50 @@
{
Ensure.NotNull(source);

return source.ToDictionary(
pair => DeepClone(pair.Key),
pair => DeepClone(pair.Value));
return CloneDictionary(source, source);
}

/// <summary>
/// Deep clones a dictionary, keeping its key comparer.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="source">The source dictionary to clone.</param>
/// <returns>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.</returns>
/// <remarks>
/// <see cref="Dictionary{TKey, TValue}"/> implements both <see cref="IDictionary{TKey, TValue}"/> and
/// <see cref="IReadOnlyDictionary{TKey, TValue}"/>, so this overload is also what lets
/// <c>dictionary.DeepClone()</c> compile without a cast.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if source is null.</exception>
public static Dictionary<TKey, TValue> DeepClone<TKey, TValue>(this Dictionary<TKey, TValue> source)
where TKey : notnull
{
Ensure.NotNull(source);

Dictionary<TKey, TValue> clone = new(source.Count, source.Comparer);
AddClonedPairs(clone, source);
return clone;
}

/// <summary>
/// Deep clones a sorted dictionary, keeping its key comparer.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="source">The source dictionary to clone.</param>
/// <returns>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.</returns>
/// <exception cref="ArgumentNullException">Thrown if source is null.</exception>
public static SortedDictionary<TKey, TValue> DeepClone<TKey, TValue>(this SortedDictionary<TKey, TValue> source)
where TKey : notnull
{
Ensure.NotNull(source);

SortedDictionary<TKey, TValue> clone = new(source.Comparer);
AddClonedPairs(clone, source);
return clone;
}

/// <summary>
Expand All @@ -153,7 +196,9 @@
/// <returns>A new read-only dictionary containing deep clones of the keys and values if they implement IDeepCloneable,
/// otherwise containing the original keys and values.</returns>
/// <remarks>
/// 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
/// <see cref="IDictionary{TKey, TValue}"/> overload, a sorted dictionary stays sorted and the source's
/// key comparer is kept when the runtime type exposes one.
///
/// Example usage:
/// <code>
Expand All @@ -166,9 +211,46 @@
{
Ensure.NotNull(source);

return source.ToDictionary(
pair => DeepClone(pair.Key),
pair => DeepClone(pair.Value));
return (IReadOnlyDictionary<TKey, TValue>)CloneDictionary(source, source);
}

/// <summary>
/// Creates an empty dictionary of the same kind, and with the same key comparer, as <paramref name="source"/>
/// where its runtime type exposes one, and fills it with deep clones of <paramref name="pairs"/>.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="source">The dictionary being cloned, inspected for its runtime type and comparer.</param>
/// <param name="pairs">The key-value pairs of the dictionary being cloned.</param>
/// <returns>A <see cref="SortedDictionary{TKey, TValue}"/> for a sorted source, otherwise a <see cref="Dictionary{TKey, TValue}"/>.
/// Both implement <see cref="IReadOnlyDictionary{TKey, TValue}"/>.</returns>
private static IDictionary<TKey, TValue> CloneDictionary<TKey, TValue>(object source, IEnumerable<KeyValuePair<TKey, TValue>> pairs)
where TKey : notnull
{
IDictionary<TKey, TValue> clone = source switch
{
SortedDictionary<TKey, TValue> sorted => new SortedDictionary<TKey, TValue>(sorted.Comparer),
Dictionary<TKey, TValue> dictionary => new Dictionary<TKey, TValue>(dictionary.Count, dictionary.Comparer),
_ => new Dictionary<TKey, TValue>(),
};
AddClonedPairs(clone, pairs);
return clone;
}

/// <summary>
/// Adds a deep clone of each key-value pair to a destination dictionary.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <param name="dest">The dictionary to add to.</param>
/// <param name="pairs">The key-value pairs to clone.</param>
private static void AddClonedPairs<TKey, TValue>(IDictionary<TKey, TValue> dest, IEnumerable<KeyValuePair<TKey, TValue>> pairs)
where TKey : notnull
{
foreach (KeyValuePair<TKey, TValue> pair in pairs)
{
dest.Add(DeepClone(pair.Key), DeepClone(pair.Value));
}
}

/// <summary>
Expand Down
Loading