如何对比两个C# 9 Record实例并找出属性差异?
用反射实现C# Record实例的通用属性差异对比
问题描述
我正在使用C# 9中的record类型,示例代码如下:
public record Receipt { public string ClientName { get; init; } = ""; public string ClientPhoneNumber { get; init; } = ""; public List<OtherRecord> OtherInfo { get; init; } = new(); }
希望编写一个函数来对比该类型的两个实例,返回所有属性值不一致的列表(用于审计场景)。是否可通过反射实现,以支持任意record类型且无需硬编码属性名?
解决方案:反射实现通用对比
完全可以通过反射实现这个需求——record本质是C#编译器生成的特殊类,反射可以正常获取其属性信息。下面是具体实现思路和代码:
核心思路
- 借助反射获取record类型的所有公共实例属性,排除编译器自动生成的内部属性(如
EqualityContract) - 遍历每个属性,获取两个实例的属性值
- 针对不同类型的值做差异化对比:
- 值类型直接用
Equals判断 - 字符串单独处理(避免引用对比)
- 集合类型对比内部元素
- 嵌套record递归对比属性
- 值类型直接用
- 收集所有不一致的属性信息,返回给调用方
完整代码实现
首先定义一个用于存储差异结果的类:
public class PropertyDifference { public string PropertyName { get; set; } public object OldValue { get; set; } public object NewValue { get; set; } }
然后实现通用对比工具类:
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; public static class RecordComparer { public static List<PropertyDifference> CompareRecords<T>(T oldRecord, T newRecord) where T : class { var differences = new List<PropertyDifference>(); // 处理实例为null的情况 if (oldRecord == null && newRecord == null) return differences; if (oldRecord == null || newRecord == null) { differences.Add(new PropertyDifference { PropertyName = "整个实例", OldValue = oldRecord, NewValue = newRecord }); return differences; } var recordType = typeof(T); // 获取所有需要对比的公共属性:排除编译器生成的EqualityContract,以及合成的属性 var properties = recordType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => !p.Name.Equals("EqualityContract", StringComparison.Ordinal)) .Where(p => !p.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), inherit: true).Any()); foreach (var property in properties) { var oldValue = property.GetValue(oldRecord); var newValue = property.GetValue(newRecord); if (!AreValuesEqual(oldValue, newValue)) { differences.Add(new PropertyDifference { PropertyName = property.Name, OldValue = oldValue, NewValue = newValue }); } } return differences; } private static bool AreValuesEqual(object oldValue, object newValue) { // 处理null场景 if (oldValue == null && newValue == null) return true; if (oldValue == null || newValue == null) return false; // 值类型直接对比 if (oldValue.GetType().IsValueType) return oldValue.Equals(newValue); // 字符串单独对比 if (oldValue is string strOld && newValue is string strNew) return strOld.Equals(strNew, StringComparison.Ordinal); // 集合类型对比元素 if (oldValue is IEnumerable<object> oldCollection && newValue is IEnumerable<object> newCollection) return oldCollection.SequenceEqual(newCollection, new RecordElementComparer()); // 嵌套record递归对比 var valueType = oldValue.GetType(); if (valueType.IsRecord()) return CompareRecords(oldValue, newValue).Count == 0; // 其他引用类型用Equals对比 return oldValue.Equals(newValue); } } // 扩展方法:判断类型是否为record public static class TypeExtensions { public static bool IsRecord(this Type type) { return type.GetCustomAttributes(typeof(System.Runtime.CompilerServices.RecordAttribute), inherit: true).Any(); } } // 集合元素对比器:支持嵌套record public class RecordElementComparer : IEqualityComparer<object> { public bool Equals(object x, object y) { return RecordComparer.AreValuesEqual(x, y); } public int GetHashCode(object obj) { return obj?.GetHashCode() ?? 0; } }
代码说明
- 排除编译器生成的属性:通过
CompilerGeneratedAttribute过滤掉record自动生成的合成属性,只保留用户定义的属性 - 嵌套record支持:递归调用对比方法,深入嵌套record的属性进行对比
- 集合处理:使用
SequenceEqual结合自定义比较器,确保集合内的元素(包括record元素)也能正确对比 - null处理:覆盖了实例为null、属性值为null的各种场景
测试示例
假设我们有嵌套的OtherRecord:
public record OtherRecord { public int Id { get; init; } public string Description { get; init; } = ""; } // 测试代码 var oldReceipt = new Receipt { ClientName = "Alice", ClientPhoneNumber = "123456", OtherInfo = new List<OtherRecord> { new OtherRecord { Id = 1, Description = "Item 1" }, new OtherRecord { Id = 2, Description = "Item 2" } } }; var newReceipt = new Receipt { ClientName = "Bob", ClientPhoneNumber = "123456", OtherInfo = new List<OtherRecord> { new OtherRecord { Id = 1, Description = "Item 1 Updated" }, new OtherRecord { Id = 3, Description = "Item 3" } } }; var differences = RecordComparer.CompareRecords(oldReceipt, newReceipt); foreach (var diff in differences) { Console.WriteLine($"属性: {diff.PropertyName}, 旧值: {diff.OldValue}, 新值: {diff.NewValue}"); }
输出结果:
属性: ClientName, 旧值: Alice, 新值: Bob 属性: OtherInfo, 旧值: System.Collections.Generic.List`1[OtherRecord], 新值: System.Collections.Generic.List`1[OtherRecord]
如果需要更详细的集合内部元素差异,可以进一步扩展代码,将集合的差异拆解为具体元素的属性变化,满足更精细的审计需求。
内容的提问来源于stack exchange,提问作者Bruno
相关产品推荐
相关产品推荐

