Skip to content

Clone the items when IEnumerable.DeepClone() is called, not on each enumeration - #85

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/80-materialize-enumerable-deepclone
Sep 27, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/80-materialize-enumerable-deepclone

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #80

What was wrong

DeepClone<T>(this IEnumerable<T>) returned source.Select(DeepClone), which is lazy. The result was a view over the live source, and each time it was enumerated every item was cloned again. Three consequences:

  • Edits to cloned items disappeared the next time the sequence was read.
  • clone.First() returned a different instance on every call.
  • The clone followed changes to the source. After src.Clear(), clone.Count() returned 0.

Lists, arrays, queues and the other collections the README lists as supported all bind to this overload, so the problem affected the library's main use case.

Change

  • The items are now cloned when DeepClone() is called ([.. source.Select(DeepClone)]). The result is a snapshot. The XML doc now says so.
  • IEnumerable<T>.DeepClone and Stack<T>.DeepClone now call Ensure.NotNull(source), like the other overloads.

Behaviour change for the changelog: the result is no longer lazy. Code that relied on the clone following the source will see a snapshot now. That is the behaviour the method's doc already promised ("A new collection containing deep clones").

Tests

  • Enumerable_DeepClone_ShouldKeepEditsAcrossEnumerations and Enumerable_DeepClone_ShouldNotTrackSourceChanges fail without the library change and pass with it.
  • Enumerable_DeepClone_NullSource_ShouldThrow and Stack_DeepClone_NullSource_ShouldThrow guard the null contract. They already passed before this change, because LINQ threw on a null source anyway.
  • dotnet test: 46/46 pass. The two multiple-enumeration tests suppress CA1851 locally, because enumerating twice is exactly what they check.

This touches a different part of DeepCloneContainerExtensions.cs from #84, so the two PRs don't conflict.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KaXGMeYeXB1J32kSrLGhgJ


Generated by Claude Code

…numeration

DeepClone(this IEnumerable<T>) returned source.Select(DeepClone), a lazy view
over the live source. Each enumeration cloned every item again, so edits to
the cloned items were lost on the next read, and the clone followed later
changes to the source. Materialize the clones when the method is called.

Also null-check the source there and in Stack<T>.DeepClone, like the other
overloads.

Fixes #80

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaXGMeYeXB1J32kSrLGhgJ
Keeps them clear of the tests other open PRs append to SpecializedCollectionTests.cs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaXGMeYeXB1J32kSrLGhgJ
@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.

IEnumerable<T>.DeepClone() returns a lazy view: edits to the cloned items are lost, and the clone changes whenever the source changes

2 participants