業務系アプリの開発をしていると、値を比較することは日常茶飯事です。
比較の実装方法は結構あります。メジャーなところでいうと、
などでしょうか。
Equalsを実装する方法は、シンプルかつ可読性が良いところはおすすめですが、フィールドの増減の都度改修が必要になります。
※Type.GetProperties()でリフレクションによる動的制御は可能ですが
IEquatable
あとはEqualsと同じです。
「シリアル化して比較」は、フィールドの増減による改修が不要な点はメリットですが、比較するクラスに[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
無事に比較できました。
値等価の比較方法をいつも迷うので、メモメモ。