【C#】比较含多次嵌套、集合、字典等的复杂对象差异
由deepseek生成的代码,基本可以比较绝大多数的对象;可以根据需要进行改造
存在两个对象,需要比较他们之间的差异
对象的属性包括
- 简单类型,int,string,bool等;
- 集合类型,数组,List,Dictionary等;
- 自定义对象类型;
输出的差异需要包括 - 差异所在路径;
- 两个差异值;
比较流程
- 两个初始对象是否为null(不可为null)
- 属性值是否为null=>其中一个为null,输出值不同
- 比较属性值类型=>输出类型名称不同;
- 比较简单类型=>输出值不懂;
- 比较集合类型=>
5.1 数组/列表,比较元素数量=>输出值元素数量不同;
比较每个元素=>输出索引下元素不同;
5.2 字典,比较所有键=>输出缺失键;
比较键对应值=>输出值不同; - 逐层递归比较嵌套对象=>输出值不同;
方法实现
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();