#region Apache License 2.0 /* Nuclex .NET Framework Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #endregion // Apache License 2.0 #if !NO_SETS using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using System.Runtime.Serialization; namespace Nuclex.Support.Cloning { partial class ExpressionTreeCloner : ICloneFactory { /// 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 field to be cloned and second when runner the matching /// cloner for the field. /// /// /// This design also enables the cloning of nested value types (which can never /// be null) without any null check whatsoever. /// /// private static Func createDeepFieldBasedCloner(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) { // Arrays need to be cloned element-by-element Type elementType = clonedType.GetElementType(); if(elementType.IsPrimitive || (elementType == typeof(string))) { // For primitive arrays, the Array.Clone() method is sufficient transferExpressions.Add( generateFieldBasedPrimitiveArrayTransferExpressions( clonedType, original, variables, transferExpressions ) ); } else { // To access the fields 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)) ); // Arrays of complex types require manual cloning transferExpressions.Add( generateFieldBasedComplexArrayTransferExpressions( clonedType, typedOriginal, 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 MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public ); transferExpressions.Add( Expression.Assign( clone, Expression.Convert( Expression.Call( getUninitializedObjectMethodInfo, Expression.Constant(clonedType) ), clonedType ) ) ); // To access the fields 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)) ); // Generate the expressions required to transfer the type field by field generateFieldBasedComplexTypeTransferExpressions( clonedType, typedOriginal, clone, variables, transferExpressions ); // 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(); } /// Compiles a method that creates a shallow clone of an object /// Type for which a clone method will be created /// A method that clones an object of the provided type private static Func createShallowFieldBasedCloner(Type clonedType) { ParameterExpression original = Expression.Parameter(typeof(object), "original"); var transferExpressions = new List(); var variables = new List(); if(clonedType.IsPrimitive || clonedType.IsValueType || (clonedType == typeof(string))) { // Primitives and strings are copied on direct assignment transferExpressions.Add(original); } else if(clonedType.IsArray) { transferExpressions.Add( generateFieldBasedPrimitiveArrayTransferExpressions( 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); // To access the fields 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)) ); // Give it a new instance of the type being cloned 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.GetFieldInfosIncludingBaseClasses( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); for(int index = 0; index < fieldInfos.Length; ++index) { FieldInfo fieldInfo = fieldInfos[index]; if(fieldInfo.IsInitOnly) { Expression source = Expression.Field(typedOriginal, fieldInfo); if(fieldInfo.FieldType.IsValueType) { source = Expression.Convert(source, typeof(object)); } if(clone.Type.IsValueType) { MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic ).MakeGenericMethod(clone.Type); transferExpressions.Add( Expression.Call( assignInitOnlyField, clone, Expression.Constant(fieldInfo), source ) ); } else { MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( "SetValue", new Type[] { typeof(object), typeof(object) } ); transferExpressions.Add( Expression.Call( Expression.Constant(fieldInfo), setValueMethodInfo, clone, source ) ); } } else { transferExpressions.Add( Expression.Assign( Expression.Field(clone, fieldInfo), Expression.Field(typedOriginal, fieldInfo) ) ); } } // 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 /// /// Type of array that will be cloned /// Variable expression for the original array /// Receives variables used by the transfer expressions /// Receives the generated transfer expressions /// The variable holding the cloned array private static Expression generateFieldBasedPrimitiveArrayTransferExpressions( Type clonedType, Expression original, ICollection variables, ICollection transferExpressions ) { MethodInfo arrayCloneMethodInfo = typeof(Array).GetMethod("Clone"); return Expression.Convert( Expression.Call( Expression.Convert(original, typeof(Array)), arrayCloneMethodInfo ), clonedType ); } /// /// Generates state transfer expressions to copy an array of complex types /// /// Type of array that will be cloned /// Variable expression for the original array /// Receives variables used by the transfer expressions /// Receives the generated transfer expressions /// The variable holding the cloned array private static ParameterExpression generateFieldBasedComplexArrayTransferExpressions( Type clonedType, Expression original, IList variables, ICollection transferExpressions ) { // We need a temporary variable in order to transfer the elements of the array ParameterExpression clone = Expression.Variable(clonedType); variables.Add(clone); int dimensionCount = clonedType.GetArrayRank(); Type elementType = clonedType.GetElementType(); var lengths = new List(); var indexes = new List(); var labels = new List(); // Retrieve the length of each of the array's dimensions MethodInfo arrayGetLengthMethodInfo = typeof(Array).GetMethod("GetLength"); for(int index = 0; index < dimensionCount; ++index) { // Obtain the length of the array in the current dimension lengths.Add(Expression.Variable(typeof(int))); variables.Add(lengths[index]); transferExpressions.Add( Expression.Assign( lengths[index], Expression.Call( original, arrayGetLengthMethodInfo, Expression.Constant(index) ) ) ); // Set up a variable to index the array in this dimension indexes.Add(Expression.Variable(typeof(int))); variables.Add(indexes[index]); // Also set up a label than can be used to break out of the dimension's // transfer loop labels.Add(Expression.Label()); } // Create a new (empty) array with the same dimensions and lengths as the original transferExpressions.Add( Expression.Assign(clone, Expression.NewArrayBounds(elementType, lengths)) ); // Initialize the indexer of the outer loop (indexers are initialized one up // in the loops (ie. before the loop using it begins), so we have to set this // one outside of the loop building code. transferExpressions.Add( Expression.Assign(indexes[0], Expression.Constant(0)) ); // Build the nested loops (one for each dimension) from the inside out Expression innerLoop = null; for(int index = dimensionCount - 1; index >= 0; --index) { var loopVariables = new List(); var loopExpressions = new List(); // If we reached the end of the current array dimension, break the loop loopExpressions.Add( Expression.IfThen( Expression.GreaterThanOrEqual(indexes[index], lengths[index]), Expression.Break(labels[index]) ) ); if(innerLoop == null) { // The innermost loop clones an actual array element if(elementType.IsPrimitive || (elementType == typeof(string))) { // Primitive array elements can be copied by simple assignment. This case // should not occur since Array.Clone() should be used instead. loopExpressions.Add( Expression.Assign( Expression.ArrayAccess(clone, indexes), Expression.ArrayAccess(original, indexes) ) ); } else if(elementType.IsValueType) { // Arrays of complex value types can be transferred by assigning all fields // of the source array element to the destination array element (cloning // any nested reference types appropriately) generateFieldBasedComplexTypeTransferExpressions( elementType, Expression.ArrayAccess(original, indexes), Expression.ArrayAccess(clone, indexes), variables, loopExpressions ); } else { // Arrays of reference types need to be cloned by creating a new instance // of the reference type and then transferring the fields over ParameterExpression originalElement = Expression.Variable(elementType); loopVariables.Add(originalElement); loopExpressions.Add( Expression.Assign(originalElement, Expression.ArrayAccess(original, indexes)) ); var nestedVariables = new List(); var nestedTransferExpressions = new List(); // A nested array should be cloned by directly creating a new array (not invoking // a cloner) since you cannot derive from an array if(elementType.IsArray) { Expression clonedElement; Type nestedElementType = elementType.GetElementType(); if(nestedElementType.IsPrimitive || (nestedElementType == typeof(string))) { clonedElement = generateFieldBasedPrimitiveArrayTransferExpressions( elementType, originalElement, nestedVariables, nestedTransferExpressions ); } else { clonedElement = generateFieldBasedComplexArrayTransferExpressions( elementType, originalElement, nestedVariables, nestedTransferExpressions ); } nestedTransferExpressions.Add( Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) ); } else { // Complex types are cloned by checking their actual, concrete type (fields // may be typed to an interface or base class) and requesting a cloner for that // type during runtime MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( "getOrCreateDeepFieldBasedCloner", BindingFlags.NonPublic | BindingFlags.Static ); MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); // Generate expressions to do this: // clone.SomeField = getOrCreateDeepFieldBasedCloner( // original.SomeField.GetType() // ).Invoke(original.SomeField); nestedTransferExpressions.Add( Expression.Assign( Expression.ArrayAccess(clone, indexes), Expression.Convert( Expression.Call( Expression.Call( getOrCreateClonerMethodInfo, Expression.Call(originalElement, getTypeMethodInfo) ), invokeMethodInfo, originalElement ), elementType ) ) ); } // Whether array-in-array of reference-type-in-array, we need a null check before // doing anything to avoid NullReferenceExceptions for unset members loopExpressions.Add( Expression.IfThen( Expression.NotEqual(originalElement, Expression.Constant(null)), Expression.Block( nestedVariables, nestedTransferExpressions ) ) ); } } else { // Outer loops of any level just reset the inner loop's indexer and execute // the inner loop loopExpressions.Add( Expression.Assign(indexes[index + 1], Expression.Constant(0)) ); loopExpressions.Add(innerLoop); } // Each time we executed the loop instructions, increment the indexer loopExpressions.Add(Expression.PreIncrementAssign(indexes[index])); // Build the loop using the expressions recorded above innerLoop = Expression.Loop( Expression.Block(loopVariables, loopExpressions), labels[index] ); } // After the loop builder has finished, the innerLoop variable contains // the entire hierarchy of nested loops, so add this to the clone expressions. transferExpressions.Add(innerLoop); return clone; } /// Generates state transfer expressions to copy a complex type /// Complex type that will be cloned /// Variable expression for the original instance /// Variable expression for the cloned instance /// Receives variables used by the transfer expressions /// Receives the generated transfer expressions private static void generateFieldBasedComplexTypeTransferExpressions( Type clonedType, // Actual, concrete type (not declared type) Expression original, // Expected to be an object Expression clone, // As actual, concrete type IList variables, ICollection transferExpressions ) { // Enumerate all of the type's fields and generate transfer expressions for each FieldInfo[] fieldInfos = clonedType.GetFieldInfosIncludingBaseClasses( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); for(int index = 0; index < fieldInfos.Length; ++index) { FieldInfo fieldInfo = fieldInfos[index]; Type fieldType = fieldInfo.FieldType; if(fieldType.IsPrimitive || (fieldType == typeof(string))) { // Primitive types and strings can be transferred by simple assignment transferExpressions.Add( Expression.Assign( Expression.Field(clone, fieldInfo), Expression.Field(original, fieldInfo) ) ); } else if(fieldType.IsValueType) { // A nested value type is part of the parent and will have its fields directly // assigned without boxing, new instance creation or anything like that. generateFieldBasedComplexTypeTransferExpressions( fieldType, Expression.Field(original, fieldInfo), Expression.Field(clone, fieldInfo), variables, transferExpressions ); } else { generateFieldBasedReferenceTypeTransferExpressions( original, clone, transferExpressions, fieldInfo, fieldType ); } } } /// /// Generates the expressions to transfer a reference type (array or class) /// /// Original value that will be cloned /// Variable that will receive the cloned value /// /// Receives the expression generated to transfer the values /// /// Reflection informations about the field being cloned /// Type of the field being cloned private static void generateFieldBasedReferenceTypeTransferExpressions( Expression original, Expression clone, ICollection transferExpressions, FieldInfo fieldInfo, Type fieldType ) { // Reference types and arrays require special care because they can be null, // so gather the transfer expressions in a separate block for the null check var fieldTransferExpressions = new List(); var fieldVariables = new List(); if(fieldType.IsArray) { // Arrays need to be cloned element-by-element Expression fieldClone; Type elementType = fieldType.GetElementType(); if(elementType.IsPrimitive || (elementType == typeof(string))) { // For primitive arrays, the Array.Clone() method is sufficient fieldClone = generateFieldBasedPrimitiveArrayTransferExpressions( fieldType, Expression.Field(original, fieldInfo), fieldVariables, fieldTransferExpressions ); } else { // Arrays of complex types require manual cloning fieldClone = generateFieldBasedComplexArrayTransferExpressions( fieldType, Expression.Field(original, fieldInfo), fieldVariables, fieldTransferExpressions ); } // Add the assignment to the transfer expressions. The array transfer expression // generator will either have set up a temporary variable to hold the array or // returned the conversion expression straight away fieldTransferExpressions.Add( Expression.Assign(Expression.Field(clone, fieldInfo), fieldClone) ); } else { // Complex types are cloned by checking their actual, concrete type (fields // may be typed to an interface or base class) and requesting a cloner for that // type during runtime MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( "getOrCreateDeepFieldBasedCloner", BindingFlags.NonPublic | BindingFlags.Static ); MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); // Equivalent to // (TField)getOrCreateDeepFieldBasedCloner( // original.SomeField.GetType() // ).Invoke(original.SomeField); Expression result = Expression.Call( Expression.Call( getOrCreateClonerMethodInfo, Expression.Call( Expression.Field(original, fieldInfo), getTypeMethodInfo ) ), invokeMethodInfo, Expression.Field(original, fieldInfo) ); // If the field is a readonly field, set the value via reflection because // Expression Trees do not support assigning .initonly fields directly yet if(fieldInfo.IsInitOnly) { if(fieldInfo.FieldType.IsValueType) { result = Expression.Convert(result, typeof(object)); } if(clone.Type.IsValueType) { MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic ).MakeGenericMethod(clone.Type); fieldTransferExpressions.Add( Expression.Call( assignInitOnlyField, clone, Expression.Constant(fieldInfo), result ) ); } else { MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( "SetValue", new Type[] { typeof(object), typeof(object) } ); fieldTransferExpressions.Add( Expression.Call( Expression.Constant(fieldInfo), setValueMethodInfo, clone, result ) ); } } else { fieldTransferExpressions.Add( Expression.Assign( Expression.Field(clone, fieldInfo), Expression.Convert(result, fieldType) ) ); } } // Wrap up the generated array or complex reference type transfer expressions // in a null check so the field is skipped if it is not holding an instance. transferExpressions.Add( Expression.IfThen( Expression.NotEqual( Expression.Field(original, fieldInfo), Expression.Constant(null) ), Expression.Block(fieldVariables, fieldTransferExpressions) ) ); } /// Assigns the value of an .initonly field /// Type of structure that contains the field /// /// Reference to the structure on which the field will be assigned /// /// Field that will be assigned /// Value that will be assigned to the field private static void assignInitOnlyField( ref TValueType instance, FieldInfo fieldInfo, object value ) where TValueType : struct { fieldInfo.SetValueDirect(__makeref(instance), value); } } } // namespace Nuclex.Support.Cloning #endif // !NO_SETS