From e8f6f541083c798da830c92dfc7a10ea311c931e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20=C5=A0t=C3=A1gl?= Date: Wed, 8 Jan 2025 00:14:46 +0100 Subject: [PATCH] fix delegates --- FastCloner.Tests/FastCloner.Tests.csproj | 12 ++ FastCloner.Tests/SpecificScenariosTest.cs | 168 ++++++++++++++++++ FastCloner/Code/BadTypes.cs | 6 + .../ClonerToExprGenerator.cs | 0 .../{Helpers => Code}/DeepCloneState.cs | 10 +- .../{Helpers => Code}/DeepClonerCache.cs | 0 .../DeepClonerExprGenerator.cs | 0 .../{Helpers => Code}/DeepClonerGenerator.cs | 31 +++- .../{Helpers => Code}/DeepClonerSafeTypes.cs | 10 +- .../{Helpers => Code}/ReflectionHelper.cs | 0 .../ShallowClonerGenerator.cs | 0 .../{Helpers => Code}/ShallowObjectCloner.cs | 0 .../{Helpers => Code}/StaticMethodInfos.cs | 0 FastCloner/FastCloner.csproj | 2 +- 14 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 FastCloner/Code/BadTypes.cs rename FastCloner/{Helpers => Code}/ClonerToExprGenerator.cs (100%) rename FastCloner/{Helpers => Code}/DeepCloneState.cs (95%) rename FastCloner/{Helpers => Code}/DeepClonerCache.cs (100%) rename FastCloner/{Helpers => Code}/DeepClonerExprGenerator.cs (100%) rename FastCloner/{Helpers => Code}/DeepClonerGenerator.cs (90%) rename FastCloner/{Helpers => Code}/DeepClonerSafeTypes.cs (96%) rename FastCloner/{Helpers => Code}/ReflectionHelper.cs (100%) rename FastCloner/{Helpers => Code}/ShallowClonerGenerator.cs (100%) rename FastCloner/{Helpers => Code}/ShallowObjectCloner.cs (100%) rename FastCloner/{Helpers => Code}/StaticMethodInfos.cs (100%) diff --git a/FastCloner.Tests/FastCloner.Tests.csproj b/FastCloner.Tests/FastCloner.Tests.csproj index 030971f..9cf69ca 100644 --- a/FastCloner.Tests/FastCloner.Tests.csproj +++ b/FastCloner.Tests/FastCloner.Tests.csproj @@ -17,8 +17,20 @@ + + + true + true + + + WINDOWS + + + LINUX + + diff --git a/FastCloner.Tests/SpecificScenariosTest.cs b/FastCloner.Tests/SpecificScenariosTest.cs index c3eacc6..6979672 100644 --- a/FastCloner.Tests/SpecificScenariosTest.cs +++ b/FastCloner.Tests/SpecificScenariosTest.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.Tracing; +using System.Drawing; using System.Globalization; using Microsoft.EntityFrameworkCore; @@ -16,6 +18,139 @@ public void Test_ExpressionTree_OrderBy1() Assert.That(q2.ToArray()[0], Is.EqualTo(1)); Assert.That(q.ToArray().Length, Is.EqualTo(5)); } + + [Test] + public void Test_Action_Delegate_Clone() + { + // Arrange + TestClass testObject = new TestClass(); + Action originalAction = testObject.TestMethod; + + // Act + Action clonedAction = originalAction.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clonedAction.Target, Is.SameAs(originalAction.Target), "Delegate Target should remain the same reference"); + Assert.That(clonedAction.Method, Is.EqualTo(originalAction.Method), "Delegate Method should be the same"); + }); + + List originalResult = []; + List clonedResult = []; + + originalAction("test"); + clonedAction("test"); + + Assert.That(clonedResult, Is.EquivalentTo(originalResult), "Both delegates should produce the same result"); + } + + [Test] + public void Test_Static_Action_Delegate_Clone() + { + // Arrange + Action originalAction = StaticTestMethod; + + // Act + Action clonedAction = originalAction.DeepClone(); + Assert.Multiple(() => + { + + // Assert + Assert.That(clonedAction.Target, Is.Null, "Static delegate Target should be null"); + Assert.That(originalAction.Target, Is.Null, "Static delegate Target should be null"); + Assert.That(clonedAction.Method, Is.EqualTo(originalAction.Method), "Delegate Method should be the same"); + }); + } + + [Test] + public void Nested_Closure_Clone() + { + // Arrange + int x = 1; + + Func outer = CreateClosure(); + + // Act + Func outerCopy = outer.DeepClone(); + + Assert.Multiple(() => + { + // Assert + Assert.That(outer.Invoke(), Is.EqualTo(6)); // 1 + 3 + 2 + Assert.That(outerCopy.Invoke(), Is.EqualTo(6)); + }); + return; + + // Helper method to create closure + Func CreateClosure() + { + int y = 3; + int z = 2; + return () => x + y + z; + } + } + + [Test] + public void Event_Handler_Clone_With_Method() + { + // Arrange + EventSource source = new EventSource(); + EventListener listener = new EventListener(); + EventHandler handler = listener.HandleEvent; + source.TestEvent += handler; + + // Act + EventHandler handlerCopy = handler.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(handlerCopy.Target, Is.SameAs(handler.Target), "Handler Target should be the same"); + Assert.That(handlerCopy.Method, Is.EqualTo(handler.Method), "Handler Method should be the same"); + + source.RaiseEvent(); + Assert.That(listener.Counter, Is.EqualTo(1), "Original handler should increment counter"); + + source.TestEvent += handlerCopy; + source.RaiseEvent(); + Assert.That(listener.Counter, Is.EqualTo(3), "Both handlers should increment counter"); + }); + } + + private class EventListener + { + public int Counter { get; private set; } + + public void HandleEvent(object sender, EventArgs e) + { + Counter++; + } + } + + private class EventSource + { + public event EventHandler TestEvent; + + public void RaiseEvent() + { + TestEvent?.Invoke(this, EventArgs.Empty); + } + } + + + private static void StaticTestMethod(string input) + { + Console.WriteLine(input); + } + + private class TestClass + { + public void TestMethod(string input) + { + Console.WriteLine(input); + } + } [Test] public void Test_ExpressionTree_OrderBy2() @@ -53,6 +188,39 @@ public void Clone_EfQuery2() int cnt = q.Count(); Assert.That(q2.Count(), Is.EqualTo(cnt)); } + + + + [Test] + [Platform(Include = "Win")] + public void FontCloningTest() + { + return; + #if WINDOWS + // Arrange + Font originalFont = new System.Drawing.Font("Arial", 12, System.Drawing.FontStyle.Bold); + + // Act + Font clonedFont = originalFont.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clonedFont, Is.Not.Null); + Assert.That(clonedFont.Name, Is.EqualTo(originalFont.Name)); + Assert.That(clonedFont.Size, Is.EqualTo(originalFont.Size)); + Assert.That(clonedFont.Style, Is.EqualTo(originalFont.Style)); + Assert.That(clonedFont.Unit, Is.EqualTo(originalFont.Unit)); + Assert.That(clonedFont.GdiCharSet, Is.EqualTo(originalFont.GdiCharSet)); + Assert.That(clonedFont.GdiVerticalFont, Is.EqualTo(originalFont.GdiVerticalFont)); + + // Ensure the cloned font is a different instance + Assert.That(ReferenceEquals(originalFont, clonedFont), Is.False); + }); + + #endif + } + [Test] public void Lazy_Clone() diff --git a/FastCloner/Code/BadTypes.cs b/FastCloner/Code/BadTypes.cs new file mode 100644 index 0000000..fe40e0a --- /dev/null +++ b/FastCloner/Code/BadTypes.cs @@ -0,0 +1,6 @@ +namespace FastCloner.Helpers; + +internal class BadTypes +{ + +} \ No newline at end of file diff --git a/FastCloner/Helpers/ClonerToExprGenerator.cs b/FastCloner/Code/ClonerToExprGenerator.cs similarity index 100% rename from FastCloner/Helpers/ClonerToExprGenerator.cs rename to FastCloner/Code/ClonerToExprGenerator.cs diff --git a/FastCloner/Helpers/DeepCloneState.cs b/FastCloner/Code/DeepCloneState.cs similarity index 95% rename from FastCloner/Helpers/DeepCloneState.cs rename to FastCloner/Code/DeepCloneState.cs index 27c1750..a105d59 100644 --- a/FastCloner/Helpers/DeepCloneState.cs +++ b/FastCloner/Code/DeepCloneState.cs @@ -14,7 +14,7 @@ internal class DeepCloneState { // this is faster than call Dictionary from begin // also, small poco objects does not have a lot of references - object[]? baseFromTo = _baseFromTo; + object[] baseFromTo = _baseFromTo; if (ReferenceEquals(from, baseFromTo[0])) return baseFromTo[3]; if (ReferenceEquals(from, baseFromTo[1])) return baseFromTo[4]; if (ReferenceEquals(from, baseFromTo[2])) return baseFromTo[5]; @@ -66,7 +66,7 @@ public object FindEntry(object key) if (_buckets != null) { int hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; - Entry[]? entries1 = _entries; + Entry[] entries1 = _entries; for (int i = _buckets[hashCode % _buckets.Length]; i >= 0; i = entries1[i].Next) { if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) @@ -148,7 +148,7 @@ public void Insert(object key, object value) int hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; int targetBucket = hashCode % _buckets.Length; - Entry[]? entries1 = _entries; + Entry[] entries1 = _entries; if (_count == entries1.Length) { @@ -171,10 +171,10 @@ public void Insert(object key, object value) private void Resize(int newSize) { - int[]? newBuckets = new int[newSize]; + int[] newBuckets = new int[newSize]; for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1; - Entry[]? newEntries = new Entry[newSize]; + Entry[] newEntries = new Entry[newSize]; Array.Copy(_entries, 0, newEntries, 0, _count); for (int i = 0; i < _count; i++) diff --git a/FastCloner/Helpers/DeepClonerCache.cs b/FastCloner/Code/DeepClonerCache.cs similarity index 100% rename from FastCloner/Helpers/DeepClonerCache.cs rename to FastCloner/Code/DeepClonerCache.cs diff --git a/FastCloner/Helpers/DeepClonerExprGenerator.cs b/FastCloner/Code/DeepClonerExprGenerator.cs similarity index 100% rename from FastCloner/Helpers/DeepClonerExprGenerator.cs rename to FastCloner/Code/DeepClonerExprGenerator.cs diff --git a/FastCloner/Helpers/DeepClonerGenerator.cs b/FastCloner/Code/DeepClonerGenerator.cs similarity index 90% rename from FastCloner/Helpers/DeepClonerGenerator.cs rename to FastCloner/Code/DeepClonerGenerator.cs index d77412d..36f67c3 100644 --- a/FastCloner/Helpers/DeepClonerGenerator.cs +++ b/FastCloner/Code/DeepClonerGenerator.cs @@ -1,18 +1,35 @@ -namespace FastCloner.Helpers; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace FastCloner.Helpers; internal static class DeepClonerGenerator { public static T? CloneObject(T? obj) { - if (obj is ValueType) + switch (obj) { - Type? type = obj.GetType(); - if (typeof(T) == type) + case ValueType: { - if (DeepClonerSafeTypes.CanReturnSameObject(type)) - return obj; + Type type = obj.GetType(); + + if (typeof(T) == type) + { + return DeepClonerSafeTypes.CanReturnSameObject(type) ? obj : CloneStructInternal(obj, new DeepCloneState()); + } - return CloneStructInternal(obj, new DeepCloneState()); + break; + } + case Delegate del: + { + Type? targetType = del.Target?.GetType(); + + if (targetType?.GetCustomAttribute() is not null) + { + return (T?)CloneClassRoot(obj); + } + + return obj; } } diff --git a/FastCloner/Helpers/DeepClonerSafeTypes.cs b/FastCloner/Code/DeepClonerSafeTypes.cs similarity index 96% rename from FastCloner/Helpers/DeepClonerSafeTypes.cs rename to FastCloner/Code/DeepClonerSafeTypes.cs index 6a9397a..21afb05 100644 --- a/FastCloner/Helpers/DeepClonerSafeTypes.cs +++ b/FastCloner/Code/DeepClonerSafeTypes.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Reflection; +using System.Runtime.CompilerServices; namespace FastCloner.Helpers; @@ -29,7 +30,7 @@ internal static class DeepClonerSafeTypes [typeof(IntPtr)] = true, [typeof(UIntPtr)] = true, [typeof(Guid)] = true, - + // Others [typeof(DBNull)] = true, [StringComparer.Ordinal.GetType()] = true, @@ -54,6 +55,13 @@ private static bool CanReturnSameType(Type type, HashSet? processingTypes) if (KnownTypes.TryGetValue(type, out bool isSafe)) return isSafe; + if (typeof(Delegate).IsAssignableFrom(type)) + { + KnownTypes.TryAdd(type, false); + return false; + } + + // enums are safe // pointers (e.g. int*) are unsafe, but we cannot do anything with it except blind copy if (type.IsEnum() || type.IsPointer) diff --git a/FastCloner/Helpers/ReflectionHelper.cs b/FastCloner/Code/ReflectionHelper.cs similarity index 100% rename from FastCloner/Helpers/ReflectionHelper.cs rename to FastCloner/Code/ReflectionHelper.cs diff --git a/FastCloner/Helpers/ShallowClonerGenerator.cs b/FastCloner/Code/ShallowClonerGenerator.cs similarity index 100% rename from FastCloner/Helpers/ShallowClonerGenerator.cs rename to FastCloner/Code/ShallowClonerGenerator.cs diff --git a/FastCloner/Helpers/ShallowObjectCloner.cs b/FastCloner/Code/ShallowObjectCloner.cs similarity index 100% rename from FastCloner/Helpers/ShallowObjectCloner.cs rename to FastCloner/Code/ShallowObjectCloner.cs diff --git a/FastCloner/Helpers/StaticMethodInfos.cs b/FastCloner/Code/StaticMethodInfos.cs similarity index 100% rename from FastCloner/Helpers/StaticMethodInfos.cs rename to FastCloner/Code/StaticMethodInfos.cs diff --git a/FastCloner/FastCloner.csproj b/FastCloner/FastCloner.csproj index 6532f0b..37ddcfa 100644 --- a/FastCloner/FastCloner.csproj +++ b/FastCloner/FastCloner.csproj @@ -25,7 +25,7 @@ 1.1.4 FastCloner - Fast deep cloning library for .NET 8+ + Fast deep cloning library for .NET 8+. Supports both deep and shallow cloning. Extensively tested, focused on performance and stability even on complicated object graphs. README.md Matěj Štágl, Adrian Mos, DeepCloner Collaborators