使用c#強大的表示式樹實現物件的深克隆之解決迴圈引用的問題

a1010發表於2024-05-15

在上一期部落格裡,我們提到使用使用c#強大的表示式樹實現物件的深克隆,文章地址:https://www.cnblogs.com/gmmy/p/18186750。但是文章裡沒有解決如何實現迴圈引用的問題。

迴圈引用

在C#中,迴圈引用通常發生在兩個或更多的物件相互持有對方的引用,從而形成一個閉環。這種情況在使用物件導向程式設計時比較常見,尤其是在處理複雜的資料結構如圖或樹時。當我們使用表示式樹進行物件建立時,如果遇到迴圈引用,很有可能導致表示式樹無限遞迴直至超出最大遞迴限制而引發溢位。

以之前的程式碼為例,這次我們引入一個迴圈引用的案例,其中型別定義如下:

public class TestDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Dictionary<string,int> Record { get; set; }
    public double[] Scores { get; set; }
    public ChildTestDto Child { get; set; }
}
public class ChildTestDto
{
    public string Name { get; set; }
    public TestDto Father { get; set; }
}

我們可以觀察到當ChildTestDto的Father被指向TestDto時,一個環狀結構就出現了。當我們使用序列化和反序列化時,很容易導致框架丟擲異常或者忽略引用(根據框架特性和配置來決定框架的行為)。那麼在表示式樹中要解決這個問題要如何處理呢?核心其實就是當我們遇到屬性指向一個型別時,我們需要檢測這個型別是否被建立過了,如果沒有被建立,我們new一個。如果已經被建立,則我們可以直接返回被建立的物件。這裡的核心關鍵是,當我們new的物件,我們需要引入【延遲】策略來進行賦值,否則建立一個新物件沒有複製原始物件的屬性,也不符合我們的要求。

那麼接下來就是如何實現【延遲】策略了,首先我們需要改造我們的DeepClone函式,因為DeepClone是外部呼叫的入口,而為了【檢測】物件,我們需要維護一個字典,所以只有在內部實現新的深克隆函式透過傳遞字典進行遞迴呼叫來實現檢測。

首先是重新定義一個新的執行緒安全字典集合用於儲存【延遲賦值】的表示式樹

public static class DeepCloneExtension
{
    //建立一個執行緒安全的快取字典,複用表示式樹
    private static readonly ConcurrentDictionary<Type, Delegate> cloneDelegateCache = new ConcurrentDictionary<Type, Delegate>();
    //建立一個執行緒安全的快取字典,複用字典延遲賦值表示式樹
    private static readonly ConcurrentDictionary<Type, Delegate> dictCopyDelegateCache = new ConcurrentDictionary<Type, Delegate>();
    //定義所有可處理的型別,透過策略模式實現了可擴充套件
    private static readonly List<ICloneHandler> handlers = new List<ICloneHandler>
    ....
}

接著我們需要從DeepClone擴充套件一個新的可以接受字典引數的內部克隆函式,定義如下:

public static T DeepClone<T>(this T original)
{
    if (original == null)
        return default;
    Dictionary<object, object> dict = new Dictionary<object, object>();
    T target = original.DeepCloneWithTracking(dict);
    return target;
}
public static T DeepCloneWithTracking<T>(this T original, Dictionary<object, object> dict)
{
    T clonedObject = Activator.CreateInstance<T>();
    var testfunc = CreateDeepCopyAction<T>();
    if (original == null)
        return default;
    if (dict.ContainsKey(original))
    {
        return (T)dict[original];
    }
    dict.Add(original, clonedObject);
    var cloneFunc = (Func<T, Dictionary<object, object>, T>)cloneDelegateCache.GetOrAdd(typeof(T), t => CreateCloneExpression<T>().Compile());
    var obj = cloneFunc(original, dict);
    var dictCopyFunc = (Action<T, T>)dictCopyDelegateCache.GetOrAdd(typeof(T), t => CreateDeepCopyAction<T>());
    dictCopyFunc(obj, clonedObject);
    return clonedObject;
}

DeepCloneWithTracking的作用就是接受一個字典,透過字典來控制物件的引用,從而實現【延遲】賦值的操作。其中的第二個關鍵點在於CreateDeepCopyAction,這個函式將建立一個淺複製,用於從深複製建立的物件中進行屬性賦值。注意這裡為什麼不直接對clonedObject進行賦值呢?這是因為當我這裡進行賦值時,是對當前clonedObject做了新的引用,而字典中儲存的是舊的引用。這就會導致【延遲】策略失效。

var a = new TestDto();
var b = a;
a = new TestDto();
a==b // false

var a = new TestDto();
var b = a;
a.Name = "xxx";
a==b //true

所以我們只能透過CreateDeepCopyAction進行淺複製操作,而不能直接進行賦值,這裡是關鍵。CreateDeepCopyAction的實現很簡單,就是建立一個表示式,透過對新舊兩個物件進行屬性的淺複製賦值,程式碼不復雜:

public static Action<T, T> CreateDeepCopyAction<T>()
{
    var sourceParameter = Expression.Parameter(typeof(T), "source");
    var targetParameter = Expression.Parameter(typeof(T), "target");
    var bindings = new List<Expression>();
    foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
    {
        if (property.CanRead && property.CanWrite)
        {
            var sourceProperty = Expression.Property(sourceParameter, property);
            var targetProperty = Expression.Property(targetParameter, property);
            var assign = Expression.Assign(targetProperty, sourceProperty);
            bindings.Add(assign);
        }
    }

    var body = Expression.Block(bindings);
    var lambda = Expression.Lambda<Action<T, T>>(body, sourceParameter, targetParameter);
    return lambda.Compile();
}

接著就是我們需要對構建表示式樹的主體邏輯進行改造,讓它支援傳遞字典,從而實現引用型別進行檢測時,傳遞字典進去,改造後的程式碼如下:

private static Expression<Func<T, Dictionary<object,object>,T>> CreateCloneExpression<T>()
{
    //反射獲取型別
    var type = typeof(T);
    // 建立一個型別為T的參數列達式 'x'
    var parameterExpression = Expression.Parameter(type, "x");
    var parameterDictExpresson = Expression.Parameter(typeof(Dictionary<object,object>), "dict");
    // 建立一個成員繫結列表,用於稍後存放屬性繫結
    var bindings = new List<MemberBinding>();
    // 遍歷型別T的所有屬性,選擇可讀寫的屬性
    foreach (var property in type.GetProperties().Where(prop => prop.CanRead && prop.CanWrite))
    {
        // 獲取原始屬性值的表示式
        var originalValue = Expression.Property(parameterExpression, property);
        // 初始化一個表示式用於存放可能處理過的屬性值
        Expression valueExpression = null;
        // 標記是否已經處理過此屬性
        bool handled = false;
        // 遍歷所有處理器,查詢可以處理當前屬性型別的處理器
        foreach (var handler in handlers)
        {
            // 如果找到合適的處理器,使用它來建立克隆表示式
            if (handler.CanHandle(property.PropertyType))
            {
                valueExpression = handler.CreateCloneExpression(originalValue, parameterDictExpresson);
                handled = true;
                break;
            }
        }
        // 如果沒有找到處理器,則使用原始屬性值
        if (!handled)
        {
            valueExpression = originalValue;
        }
        // 建立屬性的繫結
        var binding = Expression.Bind(property, valueExpression);
        // 將繫結新增到繫結列表中
        bindings.Add(binding);
    }
    // 使用所有的屬性繫結來初始化一個新的T型別的物件
    var memberInitExpression = Expression.MemberInit(Expression.New(type), bindings);
    // 建立並返回一個表示式樹,它表示從輸入引數 'x' 到新物件的轉換
    return Expression.Lambda<Func<T, Dictionary<object,object>, T>>(memberInitExpression, parameterExpression, parameterDictExpresson);
}

這裡的核心就是Lambda表示式從Func<T, T>修改成了Func<T, Dictionary<object,object>, T>,從而實現對字典的輸入。那麼同樣的,我們在具體的handler上也需要傳遞字典,如下:

interface ICloneHandler
{
    bool CanHandle(Type type);
    Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset);
}

在具體的handler編寫時,就可以傳遞這個字典:

class ClassCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        this.elementType = type;
        return type.IsClass && type != typeof(string);
    }

    public Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset)
    {
        var deepCloneMethod = typeof(DeepCloneExtension).GetMethod(nameof(DeepCloneWithTracking), BindingFlags.Public | BindingFlags.Static).MakeGenericMethod(elementType);
        return Expression.Call(deepCloneMethod, original, parameterHashset);
    }
}

其他的handler也同樣進行相關改造,比如陣列handler:

class ArrayCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        //陣列型別要特殊處理獲取其內部型別
        this.elementType = type.GetElementType();
        return type.IsArray;
    }

    public Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset)
    {
        //值型別或字串,透過值型別陣列賦值
        if (elementType.IsValueType || elementType == typeof(string))
        {
            return Expression.Call(GetType().GetMethod(nameof(DuplicateArray), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType), original);
        }
        //否則使用引用型別賦值
        else
        {
            var arrayCloneMethod = GetType().GetMethod(nameof(CloneArray), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType);
            return Expression.Call(arrayCloneMethod, original, parameterHashset);
        }
    }
    //引用型別陣列賦值
    static T[] CloneArray<T>(T[] originalArray, Dictionary<object,object> dict) where T : class, new()
    {
        if (originalArray == null)
            return null;

        var length = originalArray.Length;
        var clonedArray = new T[length];
        for (int i = 0; i < length; i++)
        {
            clonedArray[i] = DeepCloneWithTracking(originalArray[i], dict);//呼叫該型別的深克隆表示式
        }
        return clonedArray;
    }
    //值型別陣列賦值
    static T[] DuplicateArray<T>(T[] originalArray)
    {
        if (originalArray == null)
            return null;

        T[] clonedArray = new T[originalArray.Length];
        Array.Copy(originalArray, clonedArray, originalArray.Length);
        return clonedArray;
    }
}

最後實操一下,執行測試程式碼,可以看到b和b.child.father已經正確的被指向同一個引用了,和a與a.child.father一樣效果:

相關文章