Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses issue #35 by making FastCloner’s set-cloning path handle non-generic concrete types that implement ISet<T> via an interface (where Type.GetGenericArguments() can be empty), and adds regression tests for that scenario.
Changes:
- Refactors
IsSetTypeto use a sharedGetSetInterface(Type)helper. - Updates set processor generation to derive the element type from the implemented
ISet<T>interface when the concrete type has no generic arguments. - Adds tests covering cloning a non-generic
ISet<string>implementation directly and as a nested member.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/FastCloner/Code/FastClonerExprGenerator.cs | Adds GetSetInterface helper and updates set-processing to avoid failures on non-generic set implementations. |
| src/FastCloner.Tests/CollectionTests.cs | Adds a non-generic ISet<string> implementation and regression tests validating deep cloning behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Type[] genericArguments = type.GenericArguments(); | ||
| Type elementType = genericArguments.Length > 0 | ||
| ? genericArguments[0] | ||
| : setInterface.GetGenericArguments()[0]; |
There was a problem hiding this comment.
elementType is chosen from type.GenericArguments()[0] when the concrete type is generic. That can be incorrect if the set element type comes from the ISet<T> interface but isn’t the first generic parameter of the concrete type (e.g., a MySet<TKey, TValue> : ISet<TValue>). Since setInterface is already computed, derive elementType from setInterface.GetGenericArguments()[0] (or otherwise map the interface’s T) to ensure correctness for all implementations.
| Type[] genericArguments = type.GenericArguments(); | |
| Type elementType = genericArguments.Length > 0 | |
| ? genericArguments[0] | |
| : setInterface.GetGenericArguments()[0]; | |
| Type elementType = setInterface.GetGenericArguments()[0]; |
| Type? setInterface = GetSetInterface(type); | ||
| if (setInterface is null) | ||
| return GenerateMemberwiseCloner(type, position); |
There was a problem hiding this comment.
GenerateProcessMethod already branches into GenerateProcessSetMethod only when IsSetType(type) is true, and IsSetType uses GetSetInterface(type). GenerateProcessSetMethod then calls GetSetInterface(type) again and has a setInterface is null fallback that should be unreachable. Consider computing the set interface once (e.g., return it from the predicate or pass it into GenerateProcessSetMethod) to avoid the extra GetInterfaces() scan and dead branch.
| Type? setInterface = GetSetInterface(type); | |
| if (setInterface is null) | |
| return GenerateMemberwiseCloner(type, position); | |
| Type setInterface = GetSetInterface(type)!; |
Deep Clone Benchmarks
Current FastCloner vs DeepCloner
FastCloner vs latest
|
| Status | Benchmark | Delta Time | Delta Alloc |
|---|---|---|---|
| 🔴 | DynamicWithArray | +8% slower | ~same |
| ⚪ | DynamicWithDictionary | ~same | ~same |
| ⚪ | DynamicWithNestedObject | ~same | ~same |
| ⚪ | FileSpec | +5% slower | ~same |
| ⚪ | LargeEventDocument_10MB | ~same | ~same |
| ⚪ | LargeLogBatch_10MB | ~same | ~same |
| ⚪ | MediumNestedObject | +4% slower | ~same |
| ⚪ | ObjectDictionary_50 | +5% slower | ~same |
| 🔴 | ObjectList_100 | +7% slower | ~same |
| 🔴 | SmallObject | +7% slower | ~same |
| ⚪ | SmallObjectWithCollections | +3% slower | ~same |
| 🔴 | StringArray_1000 | +20% slower | ~same |
Regressions
DynamicWithArray: time +8% slower, alloc ~sameObjectList_100: time +7% slower, alloc ~sameSmallObject: time +7% slower, alloc ~sameStringArray_1000: time +20% slower, alloc ~same
Improvements
- none
Mixed changes
- none
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // TTag=List<int> (mutable, no stable hash semantics) forces the iterate-and-clone | ||
| // path rather than the memberwise fast path, exposing the wrong element type. |
There was a problem hiding this comment.
The comment claims TTag=List<int> forces the iterate-and-clone path by affecting stable hash semantics, but the set fast/slow path decision is based on the ISet element type (here string, which has stable hash semantics). Consider rewording to reflect the actual regression being tested: ensuring the ISet element type is derived from the implemented interface (not type.GetGenericArguments()[0]).
| // TTag=List<int> (mutable, no stable hash semantics) forces the iterate-and-clone | |
| // path rather than the memberwise fast path, exposing the wrong element type. | |
| // Regression test: generic type where the ISet<T> element type (string) is not | |
| // the first generic argument (TTag=List<int>). Ensures the element type is | |
| // derived from the implemented ISet<T> interface, not type.GetGenericArguments()[0]. |
No description provided.