【C#】比较含多次嵌套、集合、字典等的复杂对象差异

由deepseek生成的代码,基本可以比较绝大多数的对象;可以根据需要进行改造

存在两个对象,需要比较他们之间的差异
对象的属性包括

  • 简单类型,int,string,bool等;
  • 集合类型,数组,List,Dictionary等;
  • 自定义对象类型;
    输出的差异需要包括
  • 差异所在路径;
  • 两个差异值;

比较流程

  1. 两个初始对象是否为null(不可为null)
  2. 属性值是否为null=>其中一个为null,输出值不同
  3. 比较属性值类型=>输出类型名称不同;
  4. 比较简单类型=>输出值不懂;
  5. 比较集合类型=>
    5.1 数组/列表,比较元素数量=>输出值元素数量不同;
    比较每个元素=>输出索引下元素不同;
    5.2 字典,比较所有键=>输出缺失键;
    比较键对应值=>输出值不同;
  6. 逐层递归比较嵌套对象=>输出值不同;

方法实现

public class PropertyDifference
{
    public string PropertyPath { get; set; }
    public object Value1 { get; set; }
    public object Value2 { get; set; }
}

public static class ObjectComparer
{
    public static List<PropertyDifference> Compare<T>(T obj1, T obj2)
    {
        if (obj1 == null || obj2 == null)
            throw new ArgumentNullException("Both objects must not be null");

        var comparedObjects = new HashSet<Tuple<object, object>>();
        return CompareRecursive(obj1, obj2, comparedObjects, "");
    }

    private static List<PropertyDifference> CompareRecursive(
        object obj1, object obj2,
        HashSet<Tuple<object, object>> comparedObjects,
        string currentPath)
    {
        var differences = new List<PropertyDifference>();

        var tuple = Tuple.Create(obj1, obj2);
        if (comparedObjects.Contains(tuple)) return differences;
        comparedObjects.Add(tuple);

        // Null值处理
        if (obj1 == null || obj2 == null)
        {
            if (obj1 != obj2)
            {
                differences.Add(CreateDifference(currentPath, obj1, obj2));
            }
            return differences;
        }

        Type type1 = obj1.GetType();
        Type type2 = obj2.GetType();

        // 类型一致性检查
        if (type1 != type2)
        {
            differences.Add(new PropertyDifference
            {
                PropertyPath = currentPath,
                Value1 = type1.Name,
                Value2 = type2.Name
            });
            return differences;
        }

        // 简单类型直接比较
        if (IsSimpleType(type1))
        {
            if (!Equals(obj1, obj2))
            {
                differences.Add(CreateDifference(currentPath, obj1, obj2));
            }
            return differences;
        }

        // 处理集合类型
        if (IsCollectionType(type1))
        {
            if (IsDictionaryType(type1))
            {
                CompareDictionaries(obj1, obj2, currentPath, differences, comparedObjects);
            }
            else
            {
                CompareEnumerables(obj1, obj2, currentPath, differences, comparedObjects);
            }
            return differences;
        }

        // 复杂类型递归比较属性
        foreach (var property in type1.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (!property.CanRead) continue;

            object value1 = property.GetValue(obj1);
            object value2 = property.GetValue(obj2);

            string propertyPath = string.IsNullOrEmpty(currentPath)
                ? property.Name
                : $"{currentPath}.{property.Name}";

            differences.AddRange(CompareRecursive(value1, value2, comparedObjects, propertyPath));
        }

        return differences;
    }

    private static void CompareEnumerables(
        object enum1, object enum2,
        string currentPath,
        List<PropertyDifference> differences,
        HashSet<Tuple<object, object>> comparedObjects)
    {
        var enumerable1 = ((IEnumerable)enum1).Cast<object>().ToList();
        var enumerable2 = ((IEnumerable)enum2).Cast<object>().ToList();

        // 比较元素数量
        if (enumerable1.Count != enumerable2.Count)
        {
            differences.Add(new PropertyDifference
            {
                PropertyPath = $"{currentPath}.Count",
                Value1 = enumerable1.Count,
                Value2 = enumerable2.Count
            });
        }

        // 比较每个元素
        int maxIndex = Math.Max(enumerable1.Count, enumerable2.Count);
        for (int i = 0; i < maxIndex; i++)
        {
            string elementPath = $"{currentPath}[{i}]";
            
            object item1 = i < enumerable1.Count ? enumerable1[i] : null;
            object item2 = i < enumerable2.Count ? enumerable2[i] : null;

            differences.AddRange(CompareRecursive(item1, item2, comparedObjects, elementPath));
        }
    }

    private static void CompareDictionaries(
        object dict1, object dict2,
        string currentPath,
        List<PropertyDifference> differences,
        HashSet<Tuple<object, object>> comparedObjects)
    {
        var dictionary1 = (IDictionary)dict1;
        var dictionary2 = (IDictionary)dict2;

        // 比较字典键集合
        var allKeys = new HashSet<object>();
        foreach (var key in dictionary1.Keys) allKeys.Add(key);
        foreach (var key in dictionary2.Keys) allKeys.Add(key);

        foreach (var key in allKeys)
        {
            string keyPath = $"{currentPath}[\"{key}\"]";
            bool hasKey1 = dictionary1.Contains(key);
            bool hasKey2 = dictionary2.Contains(key);

            if (!hasKey1 || !hasKey2)
            {
                differences.Add(new PropertyDifference
                {
                    PropertyPath = keyPath,
                    Value1 = hasKey1 ? dictionary1[key] : "MISSING",
                    Value2 = hasKey2 ? dictionary2[key] : "MISSING"
                });
                continue;
            }

            object value1 = dictionary1[key];
            object value2 = dictionary2[key];

            differences.AddRange(CompareRecursive(value1, value2, comparedObjects, keyPath));
        }
    }

    private static bool IsSimpleType(Type type)
    {
        return type.IsPrimitive
               || type == typeof(string)
               || type == typeof(decimal)
               || type == typeof(DateTime)
               || type == typeof(DateTimeOffset)
               || type == typeof(TimeSpan)
               || type == typeof(Guid)
               || type.IsEnum;
    }

    private static bool IsCollectionType(Type type)
    {
        if (type == typeof(string)) return false;
        return typeof(IEnumerable).IsAssignableFrom(type);
    }

    private static bool IsDictionaryType(Type type)
    {
        return typeof(IDictionary).IsAssignableFrom(type) ||
               type.GetInterfaces().Any(i => i.IsGenericType && 
               i.GetGenericTypeDefinition() == typeof(IDictionary<,>));
    }

    private static PropertyDifference CreateDifference(string path, object v1, object v2)
    {
        return new PropertyDifference
        {
            PropertyPath = path,
            Value1 = v1 ?? "null",
            Value2 = v2 ?? "null"
        };
    }

    public static void PrintDifferences<T>(T obj1, T obj2)
    {
        var differences = Compare(obj1, obj2);

        Console.WriteLine($"Comparing {typeof(T).Name} instances:");
        Console.WriteLine(differences.Count == 0
            ? "No differences found"
            : $"Found {differences.Count} differences\n");

        foreach (var diff in differences)
        {
            Console.WriteLine($"[{diff.PropertyPath}]");
            Console.WriteLine($"  Object1: {FormatValue(diff.Value1)}");
            Console.WriteLine($"  Object2: {FormatValue(diff.Value2)}");
            Console.WriteLine(new string('-', 50));
        }
    }

    private static string FormatValue(object value)
    {
        if (value is null) return "null";
        if (value is DateTime dt) return dt.ToString("yyyy-MM-dd HH:mm:ss");
        if (value is ICollection collection) return $"{collection.Count} items";
        return value.ToString();
    }
}

测试用例

// 测试用例
public class Department
{
    public string Name { get; set; }
    public List<string> Employees { get; set; }
}

public class Company
{
    public Dictionary<int, Department> Departments { get; set; }
    public List<string> Locations { get; set; }
}

class Program
{
    static void Main()
    {
        var company1 = new Company
        {
            Departments = new Dictionary<int, Department>
            {
                [1] = new Department { Name = "Dev", Employees = new List<string> { "Alice", "Bob" } },
                [2] = new Department { Name = "HR", Employees = new List<string> { "Charlie" } }
            },
            Locations = new List<string> { "New York", "London" }
        };

        var company2 = new Company
        {
            Departments = new Dictionary<int, Department>
            {
                [1] = new Department { Name = "Development", Employees = new List<string> { "Alice", "David" } },
                [3] = new Department { Name = "Finance", Employees = new List<string>() }
            },
            Locations = new List<string> { "London", "Paris" }
        };

        ObjectComparer.PrintDifferences(company1, company2);
    }
}

使用场景

数据修改前后对比,记录修改详情

PropertyDifference可以增加DiffType枚举属性:属性编辑、集合增加、集合删除、集合增加的元素、集合删除的元素。
用于区分数据修改的类型。

修改结果类定义

public enum DiffType
{
    Edit,
    Add,
    Delete,
    AddDetail,
    DeleteDetail
}
public class PropertyDifference
{
    public DiffType DiffType { get; set; }
    public string PropertyPath { get; set; }
    public object ValueOld { get; set; }
    public object ValueCurrent { get; set; }
}

修改原方法

private static List<PropertyDifference> CompareRecursive(..){
	...
	if (old == null || curent == null)
	{
	// Null值处理
	    if (old != curent)
	    {
	    //旧值某路径下的值为空,说明是增加了一个集合元素,展示其增加元素的详情
	    //新值某路径下的值为空,说明是删除了一个集合元素,展示其删除元素的详情
	        CfgDiffType diffType = (old == null && curent != null) 
	        ? CfgDiffType.AddDetail : CfgDiffType.DeleteDetail;
	        differences.Add(CreateDifference(currentPath, old, curent,diffType));
	    }
	    return differences;
	}
	...

	// 简单类型直接比较
	if (IsSimpleType(type_old))
	{
	    if (!Equals(old, curent))
	    {
	    //简单类型出现差异,说明是编辑了一个属性值
	        differences.Add(CreateDifference(currentPath, old, curent,  CfgDiffType.Edit));
	    }
	    return differences;
	}
	...
}

private static void CompareEnumerables(...){
	// 比较元素数量
	if (enumerable_old.Count != enumerable_cur.Count)
	{
	//旧值集合数量 != 新值集合数量,说明是增加/删除了集合元素,展示其前后数量
	    differences.Add(new PropertyDifference
	    { 
	        PropertyPath = $"{currentPath}.Count",
	        ValueOld = enumerable_old.Count,
	        ValueCurrent = enumerable_cur.Count,
	        DiffType = (enumerable_old.Count < enumerable_cur.Count) ? CfgDiffType.Add : CfgDiffType.Delete
	    });
	}
}

知识点

Tuple

元组,一种包含不同数据类型的元素序列的数据结构

Tuple<int, string, string> person = 
new Tuple <int, string, string>(1, "Steve", "Jobs");

var person = Tuple.Create(1, "Steve", "Jobs");

person.Item1; // 返回 1
person.Item2; // 返回 "Steve"
person.Item3; // 返回 "Jobs"

HashSet

一个无序集合数据结构,用于存储不重复的元素集合
可以简单理解为没有Value的Dictionary<TKey,TValue>。
a. HashSet中的值不能重复且没有顺序。
b. HashSet的容量会按需自动添加。

HashSet<string> hashSet = new HashSet<string>();

hashSet.Add("A");
hashSet.Add("B");

hashSet.Contains("D");//false

hashSet.Count;

hashSet.Remove(item);

//{1,2,3}{1,2,3,4}
//numbers是oddNumbers的子集
numbers.IsProperSubsetOf(oddNumbers)

// 并集操作
numbers.UnionWith(oddNumbers);//1,2,3,4

// 交集操作
numbers.IntersectWith(oddNumbers);//1,2,3

// 差集操作
numbers.ExceptWith(oddNumbers);//4

// 对称差集操作
//{1,2,3}{1,5,3,4}
numbers.SymmetricExceptWith(oddNumbers);//2,5,4

简单应用:剔除重复值

string[] cities = new string[] {
                "Delhi",
                "Kolkata",
                "New York",
                "London",
                "Tokyo",
                "Washington",
                "Tokyo"
            };
HashSet<string> hashSet = new HashSet<string>(cities);
foreach (var city in hashSet)
{
	Console.WriteLine(city);
}

Type类的一些方法和属性

IsAssignableFrom

bool res = {TypeA}.IsAssignableFrom({TypeB}) ;
如果TypeA和TypeB类型一样则返回true;
如果TypeA是TypeB的父类则返回true;
如果TypeB实现了接口TypeA则返回true;

IsPrimitive

判断类型是否是基本类型;
.NET下的基元类型(Primitive Type)如下14个。我们可以这样来记:长度(字节数)分别为1、2、4、8的有/无符号的整数;外加两个基于指针宽度(下x86=4; x64=8)的整数,计10个。长度(字节数)分别为4和8的单精度和双精度浮点数,计2个。外加布尔类型和字符类型, 计2个。所以我们熟悉的String(string)和Decimal(decimal)并不是基元类型。

  • 整数(10):Byte(byte)/SByte(sbyte), Int16(short)/UInt16(ushort), Int32(int)/UInt32(uint), Int64(long)/UInt64(ulong), IntPtr(nint)/UIntPtr(nuint)
  • 浮点(2):Float(float), Double(double)
  • 布尔(1):Boolean(bool)
  • 字符(1):Char(char)

GetInterface()

用于获取由当前Type实现或继承的特定接口

GetGenericTypeDefinition()

返回当前构造类型的基础泛型类型定义

IsGenericType

属性用于确定一个类型是否为泛型类型

LINQ的一些用法

Cast<T>

批量强制转换类

//将enum1集合内的所有元素强制转换为object后,列表花
var enumerable1 = ((IEnumerable)enum1).Cast<object>().ToList();
posted @ 2025-04-15 10:06  Sitar  阅读(52)  评论(0)    收藏  举报