Repository navigation
Clone a collection subclass that implements IDeepCloneable by its own implementation in every overload [patch] - #150
Merged
matt-edmondson merged 2 commits intoNov 13, 2026
Conversation
… implementation in every overload [patch] #141 made only the IEnumerable<T> overload defer to a source's own IDeepCloneable.DeepClone(). A List<T>, HashSet<T>, Dictionary<,>, Queue<T> or other concrete-type subclass bound to its more specific overload and was cloned element by element, losing its type and state. A shared CloneItself helper now runs first in every collection overload whose type can be derived from. It opens the clone context, so an item the collection holds twice is cloned once, and it falls through to the element-wise clone when the implementation calls the base overload on itself, instead of recursing. Fixes #148 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vfJW3faRnes8WmPvx9zps
…s own clone SonarCloud reported 71% coverage on new code: the early return in the IDictionary, IReadOnlyDictionary, SortedDictionary, SortedList, OrderedDictionary, ConcurrentDictionary, ReadOnlyDictionary, LinkedList, SortedSet, Stack and Concurrent* overloads had no test. One test now sends a marked subclass through each of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vfJW3faRnes8WmPvx9zps
|
matt-edmondson
deleted the
fix/own-clone-for-collection-subclasses-148
branch
November 13, 2026 21:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #148
What was wrong
#141 (for #140) made only the
IEnumerable<T>extension defer to a source's ownIDeepCloneable.DeepClone(). A type such asclass Tags : List<Item>, IDeepCloneablehas no instanceDeepClone(), sotags.DeepClone()binds to the more specificList<T>overload. That overload cloned element by element into a plainList<T>, and the subclass's type and itsLabelwere lost. The same happened forHashSet,Dictionary,Queueand the other concrete overloads.The path #141 did add also called
cloneable.DeepClone()without opening the clone context. So an item the collection held twice came back as two clones, against #132.Change
CloneItself. It runs first in every collection overload whose type can be subclassed:IEnumerable,IDictionary,Dictionary,SortedDictionary,SortedList,OrderedDictionary(.NET 9+),ConcurrentDictionary,ReadOnlyDictionary,IReadOnlyDictionary,HashSet,List,Queue,LinkedList,SortedSet,Stack,ConcurrentStack,ConcurrentQueueandConcurrentBag. Arrays and the immutable and frozen types can't be subclassed, so their overloads are unchanged.CloneContext.Enter(), so one item held twice clones to one shared clone.clone.AddRange(DeepCloneContainerExtensions.DeepClone<Item>(this)). Deferring toIDeepCloneablein every overload would make that recurse until the stack overflows. A[ThreadStatic]reference set of the sources currently running their own clone handles this. Such an inner call falls through to the element-wise clone, which is what it got before.CloneContext.ReferenceComparergoes fromprivatetointernalso the guard can reuse it.#149 (registering collections in the clone context) changes the top of the same overloads. The triage suggests doing it in this helper, but it is a separate behaviour change and is left to its own PR.
CloneItselfis the natural place to add itsTryGetClone/Register.Tests
New in
CloneableCollectionTests:List_,HashSet_,Dictionary_andQueue_DeepClone_OfIDeepCloneableSubclass_ShouldUseItsOwnClone. Each is the issue'sTagsrepro through that overload. The clone must be the subclass, keepLabel == "x"and hold cloned items.Enumerable_DeepClone_OfIDeepCloneableListSubclass_ShouldUseItsOwnClone: the same subclass throughIEnumerable<T>. This already passed and stays in as a guard.Enumerable_DeepClone_OfIDeepCloneableCollectionHoldingOneObjectTwice_ShouldShareOneClone: the issue'sBagholdingxtwice.List_DeepClone_OfSubclassWhoseCloneCallsTheBaseOverload_ShouldNotRecurse: the re-entrancy guard.Without the library change, 6 of the 7 new tests fail (wrong runtime type, or two clones of the shared item). The guard test fails too, because before the change the type was lost. Full suite on Linux / .NET 10: 168/168 passed. The library builds for net10.0, net9.0, net8.0, netstandard2.1 and netstandard2.0.
Generated by Claude Code