From d091061baa21307da3e26f278639fbf8ccef0849 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 8 Feb 2012 17:38:20 +0000 Subject: [PATCH] ExpressionTreeCloner now can create shallow field-based clones of objects without a default constructor (before this, only deep clones worked); search-and-replace fix - some comments referred to something called 'propertys'; all cloners now correctly handle being passed null values (a clone of 'null' is also 'null' per definition) git-svn-id: file:///srv/devel/repo-conversion/nusu@246 d2e56fa2-650e-0410-a79f-9358c0239efd --- .../ExpressionTreeCloner.FieldBased.cs | 15 ++- .../ExpressionTreeCloner.PropertyBased.cs | 100 ++++++++++++++++-- Source/Cloning/ExpressionTreeCloner.Test.cs | 15 ++- Source/Cloning/ExpressionTreeCloner.cs | 6 +- Source/Cloning/ReflectionCloner.Test.cs | 9 ++ Source/Cloning/ReflectionCloner.cs | 27 ++++- Source/Cloning/SerializationCloner.Test.cs | 7 ++ Source/Cloning/SerializationCloner.cs | 10 ++ 8 files changed, 167 insertions(+), 22 deletions(-) diff --git a/Source/Cloning/ExpressionTreeCloner.FieldBased.cs b/Source/Cloning/ExpressionTreeCloner.FieldBased.cs index c280d28..0bf9b35 100644 --- a/Source/Cloning/ExpressionTreeCloner.FieldBased.cs +++ b/Source/Cloning/ExpressionTreeCloner.FieldBased.cs @@ -169,7 +169,20 @@ namespace Nuclex.Support.Cloning { ); // Give it a new instance of the type being cloned - transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); + MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( + "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public + ); + transferExpressions.Add( + Expression.Assign( + clone, + Expression.Convert( + Expression.Call( + getUninitializedObjectMethodInfo, Expression.Constant(clonedType) + ), + clonedType + ) + ) + ); // Enumerate all of the type's fields and generate transfer expressions for each FieldInfo[] fieldInfos = clonedType.GetFields( diff --git a/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs b/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs index a4caef2..ab5873d 100644 --- a/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs +++ b/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs @@ -65,7 +65,7 @@ namespace Nuclex.Support.Cloning { ) ); } else { - // To access the propertys of the original type, we need it to be of the actual + // To access the properties of the original type, we need it to be of the actual // type instead of an object, so perform a downcast ParameterExpression typedOriginal = Expression.Variable(clonedType); variables.Add(typedOriginal); @@ -89,7 +89,7 @@ namespace Nuclex.Support.Cloning { // Give it a new instance of the type being cloned transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); - // To access the propertys of the original type, we need it to be of the actual + // To access the properties of the original type, we need it to be of the actual // type instead of an object, so perform a downcast ParameterExpression typedOriginal = Expression.Variable(clonedType); variables.Add(typedOriginal); @@ -122,6 +122,89 @@ namespace Nuclex.Support.Cloning { return Expression.Lambda>(resultExpression, original).Compile(); } + /// Compiles a method that creates a deep clone of an object + /// Type for which a clone method will be created + /// A method that clones an object of the provided type + /// + /// + /// The 'null' check is supposed to take place before running the cloner. This + /// avoids having redundant 'null' checks on nested types - first before calling + /// GetType() on the property to be cloned and second when runner the matching + /// cloner for the property. + /// + /// + /// This design also enables the cloning of nested value types (which can never + /// be null) without any null check whatsoever. + /// + /// + private static Func createShallowPropertyBasedCloner(Type clonedType) { + ParameterExpression original = Expression.Parameter(typeof(object), "original"); + + var transferExpressions = new List(); + var variables = new List(); + + if(clonedType.IsPrimitive || (clonedType == typeof(string))) { + // Primitives and strings are copied on direct assignment + transferExpressions.Add(original); + } else if(clonedType.IsArray) { + transferExpressions.Add( + generatePropertyBasedPrimitiveArrayTransferExpressions( + clonedType, original, variables, transferExpressions + ) + ); + } else { + // We need a variable to hold the clone because due to the assignments it + // won't be last in the block when we're finished + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + // Give it a new instance of the type being cloned + transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); + + // To access the properties of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Enumerate all of the type's properties and generate transfer expressions for each + PropertyInfo[] propertyInfos = clonedType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + Expression.Property(typedOriginal, propertyInfo) + ) + ); + } + + // Make sure the clone is the last thing in the block to set the return value + transferExpressions.Add(clone); + } + + // Turn all transfer expressions into a single block if necessary + Expression resultExpression; + if((transferExpressions.Count == 1) && (variables.Count == 0)) { + resultExpression = transferExpressions[0]; + } else { + resultExpression = Expression.Block(variables, transferExpressions); + } + + // Value types require manual boxing + if(clonedType.IsValueType) { + resultExpression = Expression.Convert(resultExpression, typeof(object)); + } + + return Expression.Lambda>(resultExpression, original).Compile(); + } + /// /// Generates state transfer expressions to copy an array of primitive types /// @@ -236,7 +319,7 @@ namespace Nuclex.Support.Cloning { ) ); } else if(elementType.IsValueType) { - // Arrays of complex value types can be transferred by assigning all propertys + // Arrays of complex value types can be transferred by assigning all properties // of the source array element to the destination array element (cloning // any nested reference types appropriately) generatePropertyBasedComplexTypeTransferExpressions( @@ -249,7 +332,7 @@ namespace Nuclex.Support.Cloning { } else { // Arrays of reference types need to be cloned by creating a new instance - // of the reference type and then transferring the propertys over + // of the reference type and then transferring the properties over ParameterExpression originalElement = Expression.Variable(elementType); loopVariables.Add(originalElement); @@ -279,7 +362,7 @@ namespace Nuclex.Support.Cloning { Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) ); } else { - // Complex types are cloned by checking their actual, concrete type (propertys + // Complex types are cloned by checking their actual, concrete type (properties // may be typed to an interface or base class) and requesting a cloner for that // type during runtime MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( @@ -363,7 +446,7 @@ namespace Nuclex.Support.Cloning { IList variables, ICollection transferExpressions ) { - // Enumerate all of the type's propertys and generate transfer expressions for each + // Enumerate all of the type's properties and generate transfer expressions for each PropertyInfo[] propertyInfos = clonedType.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy @@ -395,7 +478,7 @@ namespace Nuclex.Support.Cloning { Expression.Assign(clonedProperty, Expression.New(propertyType)) ); - // A nested value type is part of the parent and will have its propertys directly + // A nested value type is part of the parent and will have its properties directly // assigned without boxing, new instance creation or anything like that. generatePropertyBasedComplexTypeTransferExpressions( propertyType, @@ -428,6 +511,7 @@ namespace Nuclex.Support.Cloning { /// /// Receives the expression generated to transfer the values /// + /// Receives variables used by the transfer expressions /// Reflection informations about the property being cloned /// Type of the property being cloned private static void generatePropertyBasedReferenceTypeTransferExpressions( @@ -480,7 +564,7 @@ namespace Nuclex.Support.Cloning { Expression.Assign(Expression.Property(clone, propertyInfo), propertyClone) ); } else { - // Complex types are cloned by checking their actual, concrete type (propertys + // Complex types are cloned by checking their actual, concrete type (properties // may be typed to an interface or base class) and requesting a cloner for that // type during runtime MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( diff --git a/Source/Cloning/ExpressionTreeCloner.Test.cs b/Source/Cloning/ExpressionTreeCloner.Test.cs index 771c3ff..5b520b3 100644 --- a/Source/Cloning/ExpressionTreeCloner.Test.cs +++ b/Source/Cloning/ExpressionTreeCloner.Test.cs @@ -36,6 +36,15 @@ namespace Nuclex.Support.Cloning { this.cloneFactory = new ExpressionTreeCloner(); } + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); + Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); + } + /// Verifies that clones of primitive types can be created [Test] public void PrimitiveTypesCanBeCloned() { @@ -150,14 +159,13 @@ namespace Nuclex.Support.Cloning { VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); } -#if false /// /// Verifies that a property-based shallow clone of a value type can be performed /// [Test] public void ShallowPropertyBasedClonesOfValueTypesCanBeMade() { HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.ShallowClone(original, true); + HierarchicalValueType clone = this.cloneFactory.ShallowPropertyClone(original); VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: true); } @@ -167,10 +175,9 @@ namespace Nuclex.Support.Cloning { [Test] public void ShallowPropertyBasedClonesOfReferenceTypesCanBeMade() { HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.ShallowClone(original, true); + HierarchicalReferenceType clone = this.cloneFactory.ShallowPropertyClone(original); VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: true); } -#endif /// /// Verifies that a property-based deep clone of a value type can be performed diff --git a/Source/Cloning/ExpressionTreeCloner.cs b/Source/Cloning/ExpressionTreeCloner.cs index 1c826ec..571e66d 100644 --- a/Source/Cloning/ExpressionTreeCloner.cs +++ b/Source/Cloning/ExpressionTreeCloner.cs @@ -101,8 +101,7 @@ namespace Nuclex.Support.Cloning { return default(TCloned); } - throw new NotImplementedException("Not implemented yet"); - Func cloner = getOrCreateShallowFieldBasedCloner(typeof(TCloned)); + Func cloner = getOrCreateShallowPropertyBasedCloner(typeof(TCloned)); return (TCloned)cloner(objectToCloneAsObject); } @@ -239,8 +238,7 @@ namespace Nuclex.Support.Cloning { Func cloner; if(!shallowPropertyBasedCloners.TryGetValue(clonedType, out cloner)) { - throw new NotImplementedException(); - //cloner = createShallowPropertyBasedCloner(clonedType); + cloner = createShallowPropertyBasedCloner(clonedType); shallowPropertyBasedCloners.TryAdd(clonedType, cloner); } diff --git a/Source/Cloning/ReflectionCloner.Test.cs b/Source/Cloning/ReflectionCloner.Test.cs index 864e3ea..6603ea4 100644 --- a/Source/Cloning/ReflectionCloner.Test.cs +++ b/Source/Cloning/ReflectionCloner.Test.cs @@ -36,6 +36,15 @@ namespace Nuclex.Support.Cloning { this.cloneFactory = new ReflectionCloner(); } + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); + Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); + } + /// Verifies that clones of primitive types can be created [Test] public void PrimitiveTypesCanBeCloned() { diff --git a/Source/Cloning/ReflectionCloner.cs b/Source/Cloning/ReflectionCloner.cs index f845308..494a2a5 100644 --- a/Source/Cloning/ReflectionCloner.cs +++ b/Source/Cloning/ReflectionCloner.cs @@ -58,9 +58,6 @@ namespace Nuclex.Support.Cloning { /// /// Type of the object that will be cloned /// Object that will be cloned - /// - /// Whether to clone the object based on its properties only - /// /// A shallow clone of the provided object public static TCloned ShallowPropertyClone(TCloned objectToClone) { Type originalType = objectToClone.GetType(); @@ -83,7 +80,12 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A deep clone of the provided object public static TCloned DeepFieldClone(TCloned objectToClone) { - return (TCloned)deepCloneSingleFieldBased(objectToClone); + object objectToCloneAsObject = objectToClone; + if(objectToClone == null) { + return default(TCloned); + } else { + return (TCloned)deepCloneSingleFieldBased(objectToCloneAsObject); + } } /// @@ -94,7 +96,12 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A deep clone of the provided object public static TCloned DeepPropertyClone(TCloned objectToClone) { - return (TCloned)deepCloneSinglePropertyBased(objectToClone); + object objectToCloneAsObject = objectToClone; + if(objectToClone == null) { + return default(TCloned); + } else { + return (TCloned)deepCloneSinglePropertyBased(objectToCloneAsObject); + } } /// @@ -104,6 +111,11 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A shallow clone of the provided object TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } return ReflectionCloner.ShallowFieldClone(objectToClone); } @@ -114,6 +126,11 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A shallow clone of the provided object TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } return ReflectionCloner.ShallowPropertyClone(objectToClone); } diff --git a/Source/Cloning/SerializationCloner.Test.cs b/Source/Cloning/SerializationCloner.Test.cs index d550892..58edcd5 100644 --- a/Source/Cloning/SerializationCloner.Test.cs +++ b/Source/Cloning/SerializationCloner.Test.cs @@ -36,6 +36,13 @@ namespace Nuclex.Support.Cloning { this.cloneFactory = new SerializationCloner(); } + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + } + /// Verifies that clones of primitive types can be created [Test] public void PrimitiveTypesCanBeCloned() { diff --git a/Source/Cloning/SerializationCloner.cs b/Source/Cloning/SerializationCloner.cs index c1dddb4..71cb1fd 100644 --- a/Source/Cloning/SerializationCloner.cs +++ b/Source/Cloning/SerializationCloner.cs @@ -244,6 +244,11 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A deep clone of the provided object public static TCloned DeepFieldClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } using(var memoryStream = new MemoryStream()) { fieldBasedFormatter.Serialize(memoryStream, objectToClone); memoryStream.Position = 0; @@ -259,6 +264,11 @@ namespace Nuclex.Support.Cloning { /// Object that will be cloned /// A deep clone of the provided object public static TCloned DeepPropertyClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } using(var memoryStream = new MemoryStream()) { propertyBasedFormatter.Serialize(memoryStream, objectToClone); memoryStream.Position = 0;