【C#】リフレクションでオブジェクトの内容を比較する

リフレクションを使ってオブジェクトを中身で比較する方法です。

C#でオブジェクトを比較するときは以下のようにコードを書くと思います。

// サンプル用のクラスと変数宣言
public class Sample
{
   public int A { get; set; }
   public int B { get; set; }
}
var s1 = new Sampme();
var s2 = new Sampme();

// (1) 同じインスタンスかどうかを比較
if(s1 == s2) Console.WriteLine("インスタンスが同じ");

// (2) 同じ内容かを比較する
if(s1.A == s2.A && s1.B == s2.B) Console.WriteLine("内容が同じ");

ですが、(2) のように内容を比較するときは IEquatable を実装したり、== と != の演算子をオーバーロードしたりして中身どうしを比較する実装って考慮することが結構多いので割とめんどくさい(し、不注意から間違いやすい)コードを書く事が多いと思います。実装方法はだいたいこのページに書いてある通りです

なので面倒なのでリフレクションで、そういう実装をせずにオブジェクトの内容同士を比較しようと思います。

確認環境

確認環境は以下の通りです。

  • .NET Core3.1 + C# 8.0
  • VisuaStudio2019
  • Windows10

使用方法とサンプル

まずは成果物の実装の使い方と説明をしたいと思います。

使用するにあたって以下の条件があります。

  • 比較可能なオブジェクトは IReflectionEquatable を継承していること
  • 比較するフィールドもしくはプロパティに TargetAttribute を付けること
    • もしかすると IgnoreAttribute で基本全部比較するけど Ignore だけ対象にしないの方がいいかも?

で、使い方は以下の通り。

// まず比較したいクラスを以下のように宣言する

// 親のオブジェクト
public struct Value : IReflectionEquatable // ★(1) 比較できる型はインターフェースを継承する
{
    [Target] public SubValue PA { get; set; } // ★(2) 較したいプロパティに Target 属性を付ける
    [Target] public string PB { get; set; }
    [Target] private int pc { get; set; }

    [Target] public SubValue FA; // ★(3) 比較したいフィールドに Target 属性を付ける
    [Target] public string FB;
    [Target] private int fc;
    public void SetPC(int value) => this.pc = value;
    public void SetFC(int value) => this.fc = value;
}

// 子のオブジェクト
public struct SubValue : IReflectionEquatable
{
    [Target] public string PAS { get; set; }
    [Target] private int pbs { get; set; }
    [Target] public string FAS;
    [Target] private int fbs;
    public void SetPBS(int value) => this.pbs = value;
    public void SetFBS(int value) => this.fbs = value;
}

比較は以下のように行います。

public static void Main(string[] args)
{
    // テスト用データの取得
    (Value a, Value b) = createValue_1();
    
    // ★★★オブジェクトどうしの比較
    //   → Target属性がついているメンバーを全部比較してくれる
    bool ret = a.Equals<Value>(b);
    // ret = true
}

// テストデータ作成用のメソッド
private static (Value a, Value b) createValue_1()
{
    var sa = new SubValue() { PAS = "aaa", FAS = "bbb", };
    sa.SetPBS(10);
    sa.SetFBS(11);
    var fa = new SubValue() { PAS = "ccc", FAS = "ddd", };
    fa.SetPBS(12);
    fa.SetFBS(13);
    var a = new Value()
    {
        PA = sa,
        PB = "ccc",
        FA = fa,
        FB = "ddd",
    };
    a.SetPC(14);
    a.SetFC(15);

    var sb = new SubValue() { PAS = "aaa", FAS = "bbb", };
    sb.SetPBS(10);
    sb.SetFBS(11);
    var fb = new SubValue() { PAS = "ccc", FAS = "ddd", };
    fb.SetPBS(12);
    fb.SetFBS(13);
    var b = new Value()
    {
        PA = sb,
        PB = "ccc",
        FA = fb,
        FB = "ddd",
    };
    b.SetPC(14);
    b.SetFC(15);

    return (a, b);
}

実装コード

実装コードは以下の通りです。

リフレクションなので処理速度がすっごい遅いです。1秒間に1000回とか呼び出して使用するとパフォーマンスが残念な可能性があります

IReflectionEquatable

比較対象に付与するインターフェースの定義です。何もメソッドが無いマーク用のインターフェースです。

// IReflectionEquatable.cs

/// <summary>
/// リフレクションによって等値比較を行うことができることを表すマーカーインターフェース
/// </summary>
public interface IReflectionEquatable { }

TargetAttribute

比較したいプロパティ or フィールドに付与するための属性です。

// TargetAttribute.cs

/// <summary>
/// <see cref="IReflectionEquatable"/> を使用した比較対象であることを表します。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class TargetAttribute : Attribute { }

IReflectionEquatableExtension

コード例の「bool ret = a.Equals(b);」できるようにするための実装です。

プロパティとフィールドを全部列挙して比較する。IReflectionEquatable があれば再帰してさらに中身を比較する処理になっています。

// IReflectionEquatableExtension.cs

/// <summary>
/// <see cref="IReflectionEquatable"/> に対する拡張メソッドを定義します。
/// </summary>
public static class IReflectionEquatableExtension
{
    /// <summary>
    /// 指定したオブジェクトと内容を比較します。
    /// </summary>
    public static bool Equals<T>(this T self, T target) where T : IReflectionEquatable
    {
        return equals(self, target);
    }

    // 再帰的にオブジェクトを比較する処理
    private static bool equals<T>(T a, T b) where T : IReflectionEquatable
    {
        if (a == null && b != null || a != null && b == null)
        {
            return false;
        }
        else if (a == null && b == null)
        {
            return true;
        }

        var type = a.GetType();

        // 対象プロパティの列挙
        foreach (PropertyInfo p in getProperties(type))
        {
            var pa = p.GetValue(a);
            var pb = p.GetValue(b);
            //Console.WriteLine($"[Property] {p.Name}: a={pa}, b={pb}");

            if (pa is IReflectionEquatable _pa)
            {
                bool ret = equals(_pa, pb as IReflectionEquatable);
                if (!ret)
                {
                    //Console.WriteLine($"false");
                    return false;
                }
            }

            if (pa?.Equals(pb) == false)
            {
                //Console.WriteLine($"false");
                return false;
            }
        }

        // 対象フィールドの列挙
        foreach (FieldInfo f in getFields(type))
        {
            var fa = f.GetValue(a);
            var fb = f.GetValue(b);
            //Console.WriteLine($"[Field] {f.Name}: a={fa}, b={fb}");

            if (fa is IReflectionEquatable _fa)
            {
                bool ret = equals(_fa, fb as IReflectionEquatable);
                if (!ret)
                {
                    //Console.WriteLine($"false");
                    return false;
                }
            }

            if (fa?.Equals(fb) == false)
            {
                //Console.WriteLine($"false");
                return false;
            }
        }

        return true;
    }

    // 対象プロパティを全部列挙
    public static IEnumerable<PropertyInfo> getProperties(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetProperties(attributes).Where(p => p.GetCustomAttribute<TargetAttribute>() != null);
    }

    // 対象フィールドを全部列挙
    public static IEnumerable<FieldInfo> getFields(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetFields(attributes).Where(p => p.GetCustomAttribute<TargetAttribute>() != null);
    }
}

以上です。