.net

【.net】2つのオブジェクトの値を比較する方法

業務系アプリの開発をしていると、値を比較することは日常茶飯事です。
比較の実装方法は結構あります。メジャーなところでいうと、

  • EqualsをOverrideして個別に実装して比較
  • IEquatableを実装し、やはりEqualsをOverrideして個別に実装して比較
  • シリアル化して比較

などでしょうか。
Equalsを実装する方法は、シンプルかつ可読性が良いところはおすすめですが、フィールドの増減の都度改修が必要になります。
※Type.GetProperties()でリフレクションによる動的制御は可能ですが

IEquatableを使用するメリットは、タイプセーフとなることで、型の比較が不要になります。
あとはEqualsと同じです。

「シリアル化して比較」は、フィールドの増減による改修が不要な点はメリットですが、比較するクラスに[Serializable]属性を付与する必要があります。

個人的には、どの方法でも優劣ないように思います。
今回は、既存のプロジェクトに処理を追加する、というケースに比較的便利な「シリアル化して比較」のサンプルを紹介します。


比較したい型にSerializable属性を付加する

こんなクラスがあるとしましょう。
Person.cs


using System;
using System.Collections.Generic;

namespace EqualsExample
{
    [Serializable]
    public class Person
    {
        public string name { get; set; }
        
        public int age { get; set; }
        
        public List<Person> children { get; set; }
    }
}

続いてVB版
Person.vb


Namespace EqualsExample
    <Serializable>
    Public Class Person
        Public Property Name() As String
            Get
                Return _name
            End Get
            Set
                _name = Value
            End Set
        End Property
        Private _name As String

        Public Property Age() As Integer
            Get
                Return _age
            End Get
            Set
                _age = Value
            End Set
        End Property
        Private _age As Integer

        Public Property Children() As List(Of Person)
            Get
                Return _children
            End Get
            Set
                _children = Value
            End Set
        End Property
        Private _children As List(Of Person)
    End Class
End Namespace

クラス名の上に[Serializable](VBは<Serializable>)を付け、「シリアル化可能なクラスだよ」とマークします。


比較処理を作成する

どのような型でも使えるのが理想なので、拡張メソッド<T>で実装します。
BinaryFormatterでシリアライズしたMemoryStreamを比較に使用します。

Extention.cs


using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;

namespace EqualsExample
{
    public static class Extensions
    {
        public static bool ValueEquals<T>(this T self, T other)
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream msA = new MemoryStream())
            {
                bf.Serialize(msA, self);
                using (MemoryStream msB = new MemoryStream())
                {
                    bf.Serialize(msB, other);
                    msA.Position = 0;
                    msB.Position = 0;
                    byte[] a = new byte[msA.Length];
                    byte[] b = new byte[msB.Length];
                    msA.Read(a, 0, a.Length);
                    msB.Read(b, 0, b.Length);
                    return a.SequenceEqual(b);
                }
            }
        }
    }
}

VB版のモジュール(※拡張メソッドはModuleのみ作成可のため)
Extention.vb


Imports System.IO
Imports System.Runtime.CompilerServices
Imports System.Runtime.Serialization.Formatters.Binary

Module Extensions
    <Extension>
    Public Function ValueEquals(Of T)(self As T, other As T) As Boolean
        Dim bf As New BinaryFormatter()
        Using msA As New MemoryStream()
            bf.Serialize(msA, self)
            Using msB As New MemoryStream()
                bf.Serialize(msB, other)
                msA.Position = 0
                msB.Position = 0
                Dim a As Byte() = New Byte(msA.Length - 1) {}
                Dim b As Byte() = New Byte(msB.Length - 1) {}
                msA.Read(a, 0, a.Length)
                msB.Read(b, 0, b.Length)
                Return a.SequenceEqual(b)
            End Using
        End Using
    End Function
End Module

こうしておけば、Serializableをつけたすべての型に使用できます。
また、自作したクラスが更に自作したクラスのコレクション等を持っている場合、すべてにEqualsメソッドを定義しなくて済むので楽に比較できます。


使い方

「{対象の型}.」で補完に「ValueEquals」が表示されましたね?
以下比較サンプル。


using System;
using System.Windows.Forms;
using System.Collections.Generic;

namespace EqualsExample
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, System.EventArgs e)
        {
            Person p1 = new Person(){ age = 11, name = "三郎", children = null };
            Person p2 = new Person(){ age = 11, name = "三郎", children = null };
            Person p3 = new Person(){ age = 11, name = "四郎", children = null };
            Person p4 = new Person(){ age = 30, name = "太郎", children = 
                new List<Person>() { new Person() { age = 3, name = "花子", children = null } }
            };
            Person p5 = new Person(){ age = 30, name = "太郎", children = 
                new List<Person>() { new Person() { age = 3, name = "花子", children = null } }
            };
            Person p6 = new Person(){ age = 30, name = "太郎", children = 
                new List<Person>() { new Person() { age = 3, name = "花子", children = null },
                                     new Person() { age = 1, name = "富士子", children = null }}
            };

            Console.WriteLine($"三郎と三郎は同一人物?:{ p1.ValueEquals(p2)}");
            Console.WriteLine($"三郎と四郎は同一人物?:{ p1.ValueEquals(p3)}");
            Console.WriteLine($"太郎と太郎は同一人物?:{ p4.ValueEquals(p5)}");
            Console.WriteLine($"太郎と太郎は同一人物?:{ p4.ValueEquals(p6)}");
        }
    }
}

三郎と三郎は同一人物?:True
三郎と四郎は同一人物?:False
太郎と太郎は同一人物?:True
太郎と太郎は同一人物?:False

無事に比較できました。
値等価の比較方法をいつも迷うので、メモメモ。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です