1. Basic concepts of expression trees
An expression tree is an expression represented in a tree-like structure, where each node represents a part of the expression. For example, an arithmetic expressiona + b
can be represented as a tree where the root node is an addition operator, and its two child nodes area
andb
. In LINQ (Language Integrated Query), the expression tree enables the conversion of queries in C# into other forms of queries, such as SQL queries. In this way, the same query logic can be used for different types of data sources, such as databases, XML files, etc. Since expression trees can be created and modified at runtime, they are also very suitable for scenarios where code logic needs to be dynamically generated or changed based on runtime data. This is very useful for logic that requires repeated execution (such as deep clones mentioned in this article) because they can be optimized and cached, thereby increasing efficiency.
2. Create and use expression trees
In C#, we can passClasses in namespaces to create and manipulate expression trees. Here is an example of creating a simple expression tree:
// Create an expression tree to represent a + b ParameterExpression a = (typeof(int), "a"); ParameterExpression b = (typeof(int), "b"); BinaryExpression body = (a, b); // Compile expression tree as executable code var add = <Func<int, int, int>>(body, a, b).Compile(); // Use expressions (add(1, 2)); // Output 3
After we define a type, we can implement deep cloning through an anonymous delegate to perform value copying:
//Custom typepublic class TestDto { public int Id { get; set; } public string Name { get; set; } } //Anonymous commissionFunc<TestDto, TestDto> deepCopy = x => new TestDto() { Id = , Name = }; //Use itvar a =new TestDto(){//Assign value};var b = deepCopy(a);//Implement deep cloning
Then if you want to automatically create this anonymous delegation, you will use the expression tree to automatically create anonymous delegation, so that you can realize complex automated expression creation without relying on performance-consuming methods such as reflection, serialization/deserialization, etc. The core business logic part is as follows: First of all, we need to know that the expression tree traverses the object's properties through reflection to implement the assignment operation such as x =. For different attributes such as arrays, dictionaries, value types, custom classes, and strings, the assignment schemes are different. We can directly assign simple value types and strings, because the assignments of both are "deep" cloned. That is, the variable modification after assignment will not affect the original variable. If complex dictionaries, arrays, and objects are assigned with =, they will only get references to the object, so different processing is required for different situations.
First, we need to define an interface ICloneHandler, and use a processing class that inherits the interface to handle it for different situations:
interface ICloneHandler { bool CanHandle(Type type);//Is the current type possible? Expression CreateCloneExpression(Expression original);//Generate expression tree for the current type}
Next we define an extension class and extension function for handling deep copy:
public static class DeepCloneExtension { //Create a thread-safe cache dictionary and multiplex expression tree private static readonly ConcurrentDictionary<Type, Delegate> cloneDelegateCache = new ConcurrentDictionary<Type, Delegate>(); //Define all types that can be processed, and achieve scalability through policy mode private static readonly List<ICloneHandler> handlers = new List<ICloneHandler> { //Add a custom type processor here }; /// <summary> /// Deep cloning function /// </summary> /// <typeparam name="T"></typeparam> /// <param name="original"></param> /// <returns></returns> public static T DeepClone<T>(this T original) { if (original == null) return default; // Get or create cloned expression var cloneFunc = (Func<T, T>)(typeof(T), t => CreateCloneExpression<T>().Compile()); //Call the expression and return the result return cloneFunc(original); } /// <summary> /// Building the body logic of the expression tree /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> private static Expression<Func<T, T>> CreateCloneExpression<T>() { //Reflection Obtain Type var type = typeof(T); // Create a parameter expression of type T 'x' var parameterExpression = (type, "x"); // Create a member binding list to store attribute bindings later var bindings = new List<MemberBinding>(); // traverse all properties of type T and select readable and writeable properties foreach (var property in ().Where(prop => && )) { // Get the expression of the original attribute value var originalValue = (parameterExpression, property); // Initialize an expression to store the attribute values that may have been processed Expression valueExpression = null; // Tag whether this attribute has been processed bool handled = false; // traverse all processors and find processors that can handle the current attribute type foreach (var handler in handlers) { // If you find a suitable processor, use it to create a cloned expression if (()) { valueExpression = (originalValue); handled = true; break; } } // If no processor is found, use the original property value if (!handled) { valueExpression = originalValue; } // Create a property binding var binding = (property, valueExpression); // Add binding to the binding list (binding); } // Use all attribute bindings to initialize a new T-type object var memberInitExpression = ((type), bindings); // Create and return an expression tree that represents the conversion from the input parameter 'x' to the new object return <Func<T, T>>(memberInitExpression, parameterExpression); } }
Next we can add some common types of processors:
Array processing:
class ArrayCloneHandler : ICloneHandler { Type elementType; public bool CanHandle(Type type) { //Array type needs to be processed specifically to obtain its internal type = (); return ; } public Expression CreateCloneExpression(Expression original) { //Value type or string, assign value through value type array if ( || elementType == typeof(string)) { return (GetType().GetMethod(nameof(DuplicateArray), | ).MakeGenericMethod(elementType), original); } // Otherwise, use the reference type to assign values else { var arrayCloneMethod = GetType().GetMethod(nameof(CloneArray), | ).MakeGenericMethod(elementType); return (arrayCloneMethod, original); } } //Array of reference type static T[] CloneArray<T>(T[] originalArray) where T : class, new() { if (originalArray == null) return null; var length = ; var clonedArray = new T[length]; for (int i = 0; i < length; i++) { clonedArray[i] = DeepClone(originalArray[i]);//Calling deep cloning expression of this type } return clonedArray; } //Array of value type static T[] DuplicateArray<T>(T[] originalArray) { if (originalArray == null) return null; T[] clonedArray = new T[]; (originalArray, clonedArray, ); return clonedArray; } }
Custom type processing (actually, it is to call the deep clone of this type):
class ClassCloneHandler : ICloneHandler { Type elementType; public bool CanHandle(Type type) { = type; return && type != typeof(string); } public Expression CreateCloneExpression(Expression original) { var deepCloneMethod = typeof(DeepCloneExtension).GetMethod(nameof(DeepClone), | ).MakeGenericMethod(elementType); return (deepCloneMethod, original); } }
Then we can add these handles in the previous DeepCloneExtension
private static readonly List<ICloneHandler> handlers = new List<ICloneHandler> { new ArrayCloneHandler(),//Array new DictionaryCloneHandler(),//dictionary new ClassCloneHandler()//kind ... };
Finally, we can implement deep cloning by simply making calls
var a = new TestDto() { Id = 1, Name = "Xiao Ming", Child = new TestDto() { Id = 2, Name = "Little Red" }, Record = new Dictionary<string, int>() { { "Grade 1", 1 }, { "Grade 2", 2 } }, Scores = [100, 95] }; var b = ();
In short, the expression tree of C# provides a powerful mechanism to represent the code in the form of a data structure, so that the code can be checked, modified or executed at runtime. This provides many possibilities for dynamic query generation, code optimization, and dynamic programming.
This is the end of this article about using C#'s powerful expression tree to implement deep cloning objects. For more related deep cloning content of C# objects, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!