使用c#強大的表達式樹實現對象的深克隆之解決循環引用的問題

在上一期博客裏,我們提到使用使用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一樣效果:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章