You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对比两个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#编译器生成的特殊类,反射可以正常获取其属性信息。下面是具体实现思路和代码:

核心思路

  1. 借助反射获取record类型的所有公共实例属性,排除编译器自动生成的内部属性(如EqualityContract)
  2. 遍历每个属性,获取两个实例的属性值
  3. 针对不同类型的值做差异化对比:
    • 值类型直接用Equals判断
    • 字符串单独处理(避免引用对比)
    • 集合类型对比内部元素
    • 嵌套record递归对比属性
  4. 收集所有不一致的属性信息,返回给调用方

完整代码实现

首先定义一个用于存储差异结果的类:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 16:40:37