Object.Equalsメソッドを使うことで二つのオブジェクトが等しいかどうか単純な比較を行うことが出来ますが、どのような時に等しいとみなすか定義することが必要になる場合があります。
ここでは、オブジェクトの等価性を定義したり比較するためのインターフェイスであるIEquatable・IEqualityComparerと、関連するクラスについて見ていきます。 また、等価演算子のオーバーロードとこれらのインターフェイスの実装についても触れています。
Object.Equals
まずは、Object.Equalsメソッドとオブジェクトの等価性について見ていきます。 Equalsメソッドは現在のインスタンスと引数で指定されたオブジェクトとの等価性を比較するメソッドですが、Equalsメソッドをオーバーライドしない場合のデフォルトの動作は次のようになっています。 (値型と参照型 §.同値性・同一性の比較)
- 値型(struct)の場合
- 二つの値型オブジェクトがビット単位で等しい場合にtrue、そうでなければfalseを返す
- 参照型(class)の場合
- 二つの参照型オブジェクトが同一のオブジェクト(=参照先のオブジェクトが同一)である場合にtrue、そうでなければfalseを返す
そのため、次の例ではAccountクラスはEqualsメソッドをオーバーライドしていないため、各フィールドが同じ値を持つインスタンスでも異なるインスタンスの場合である限り等しいとは判断されません。 これにより、Array.IndexOfメソッドによる検索でも配列内には同一のインスタンスは存在しないため-1が返されます。
using System;
class Account {
int id;
string name;
public Account(int id, string name)
{
this.id = id;
this.name = name;
}
public override string ToString()
{
return string.Format("{0}:{1}", id, name);
}
}
class Sample {
static void Main()
{
Account[] arr = new Account[] {
new Account(1, "Eve"),
new Account(4, "Dave"),
new Account(2, "Alice"),
new Account(0, "Charlie"),
new Account(3, "Bob"),
};
Account charlie = new Account(0, "Charlie");
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
}
}
Imports System
Class Account
Dim id As Integer
Dim name As String
Public Sub New(ByVal id As Integer, ByVal name As String)
MyClass.id = id
MyClass.name = name
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}", id, name)
End Function
End Class
Class Sample
Shared Sub Main()
Dim arr() As Account = New Account() { _
New Account(1, "Eve"), _
New Account(4, "Dave"), _
New Account(2, "Alice"), _
New Account(0, "Charlie"), _
New Account(3, "Bob") _
}
Dim charlie As New Account(0, "Charlie")
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr(3), charlie.Equals(arr(3)))
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie))
End Sub
End Class
'0:Charlie' Equals '0:Charlie': False IndexOf '0:Charlie': -1
Equalsメソッドをオーバーライドすることで、どのような場合にオブジェクトが等しいとするかを定義することが出来ます。 次の例は、上記の例を書き換えEqualsメソッドをオーバーライドするようにしたものです。
using System;
class Account {
int id;
string name;
public Account(int id, string name)
{
this.id = id;
this.name = name;
}
public override string ToString()
{
return string.Format("{0}:{1}", id, name);
}
public override bool Equals(object obj)
{
// Accountクラスに型変換
Account other = obj as Account;
if (other == null) {
// objがAccountクラスでない場合、またはnullの場合は等しくないとする
return false;
}
else {
// objがAccountクラスの場合、かつフィールドidとnameの値が等しい場合は、
// 2つのオブジェクトが等しいとする
return this.id == other.id && this.name == other.name;
}
}
}
class Sample {
static void Main()
{
Account[] arr = new Account[] {
new Account(1, "Eve"),
new Account(4, "Dave"),
new Account(2, "Alice"),
new Account(0, "Charlie"),
new Account(3, "Bob"),
};
Account charlie = new Account(0, "Charlie");
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
}
}
Imports System
Class Account
Dim id As Integer
Dim name As String
Public Sub New(ByVal id As Integer, ByVal name As String)
MyClass.id = id
MyClass.name = name
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}", id, name)
End Function
Public Overrides Function Equals(ByVal obj As Object) As Boolean
' Accountクラスに型変換
Dim other As Account = TryCast(obj, Account)
If other Is Nothing Then
' objがAccountクラスでない場合、またはNothingの場合は等しくないとする
Return False
Else
' objがAccountクラスの場合、かつフィールドidとnameの値が等しい場合は、
' 2つのオブジェクトが等しいとする
Return MyClass.id = other.id AndAlso MyClass.name = other.name
End If
End Function
End Class
Class Sample
Shared Sub Main()
Dim arr() As Account = New Account() { _
New Account(1, "Eve"), _
New Account(4, "Dave"), _
New Account(2, "Alice"), _
New Account(0, "Charlie"), _
New Account(3, "Bob") _
}
Dim charlie As New Account(0, "Charlie")
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr(3), charlie.Equals(arr(3)))
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie))
End Sub
End Class
'0:Charlie' Equals '0:Charlie': True IndexOf '0:Charlie': 3
結果を見て分かる通り、二つの異なるインスタンスでも各フィールドの値が等しければ、二つのインスタンスは等しいと扱われるようになりました。 また、これにより、Array.IndexOfメソッドによる検索でも配列内にある等しいインスタンスのインデックスが返されるようになりました。
なお、Equalsメソッドをオーバーライドする場合はObject.GetHashCodeメソッドもオーバーライドする必要があります。 そのため、上記の例をコンパイルすると警告(CS0659)が出ます。 GetHashCodeが返す値は値型では特に重要になりますが、ここでは実装を省略しています。 詳しくはGetHashCodeメソッドの解説を参照してください。
IEquatable<T>
IEquatable<T>インターフェイス(System名前空間)は、型パラメータTで指定された型との等価性の比較が可能であることを表すインターフェイスです。 Object.Equalsメソッドがobject型の引数を取るのに対し、IEquatable<T>.Equalsメソッドでは型Tを引数に取ります。 このため、Object.Equalsメソッドをオーバーライドする場合とは異なり、型チェックの必要が無くなります。
以下の例は、先の例をIEquatable<T>インターフェイスを実装したものに書き換えたものです。
using System;
class Account : IEquatable<Account> {
int id;
string name;
public Account(int id, string name)
{
this.id = id;
this.name = name;
}
public override string ToString()
{
return string.Format("{0}:{1}", id, name);
}
// IEquatable<Account>.Equalsの実装
public bool Equals(Account other)
{
if (other == null) return false;
return this.id == other.id && this.name == other.name;
}
}
class Sample {
static void Main()
{
Account[] arr = new Account[] {
new Account(1, "Eve"),
new Account(4, "Dave"),
new Account(2, "Alice"),
new Account(0, "Charlie"),
new Account(3, "Bob"),
};
Account charlie = new Account(0, "Charlie");
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
// objectにキャスト
object obj = charlie;
Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr[3], obj.Equals(arr[3]));
}
}
Imports System
Class Account
Implements IEquatable(Of Account)
Dim id As Integer
Dim name As String
Public Sub New(ByVal id As Integer, ByVal name As String)
MyClass.id = id
MyClass.name = name
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}", id, name)
End Function
' IEquatable(Of Account).Equalsの実装
Public Function Equals(ByVal other As Account) As Boolean Implements IEquatable(Of Account).Equals
If other is Nothing Then Return False
Return MyClass.id = other.id AndAlso MyClass.name = other.name
End Function
End Class
Class Sample
Shared Sub Main()
Dim arr() As Account = New Account() { _
New Account(1, "Eve"), _
New Account(4, "Dave"), _
New Account(2, "Alice"), _
New Account(0, "Charlie"), _
New Account(3, "Bob") _
}
Dim charlie As New Account(0, "Charlie")
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr(3), charlie.Equals(arr(3)))
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie))
' Objectにキャスト
Dim obj As Object = charlie
Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr(3), obj.Equals(arr(3)))
End Sub
End Class
'0:Charlie' Equals '0:Charlie': True IndexOf '0:Charlie': -1 '0:Charlie' Equals '0:Charlie': False
単純にAccount.Equalsメソッドを呼び出した比較は正しく動作していますが、結果を見て分かる通りArray.IndexOfメソッドの結果は意図に反して-1を返しています。 これは、Array.IndexOfメソッドではオブジェクトがIEquatable<T>.Equalsメソッドを実装しているかどうかに関わらず、常にObject.Equalsメソッドを使って等価性の比較を行うためです。 AccountクラスはObject.Equalsをオーバーライドしていないため、このような結果となります。 objectにキャストしてからEqualsメソッドで比較した場合も同様に、意図に反してfalseを返しています。
このように、型がIEquatable<T>を実装していても、比較する側がIEquatable<T>.Equalsメソッドを呼び出すようになっていない限り、意図しない結果となります。 そのため、多くの場合はIEquatable<T>を実装すると同時に、Object.Equalsメソッドもオーバーライドすることになります。 以下の例は、上記の例にObject.Equalsメソッドのオーバーライドを追加したものです。
using System;
class Account : IEquatable<Account> {
int id;
string name;
public Account(int id, string name)
{
this.id = id;
this.name = name;
}
public override string ToString()
{
return string.Format("{0}:{1}", id, name);
}
// IEquatable<Account>.Equalsの実装
public bool Equals(Account other)
{
if (other == null) return false;
return this.id == other.id && this.name == other.name;
}
// Object.Equalsをオーバーライド
public override bool Equals(object obj)
{
// Accountに型変換
Account other = obj as Account;
// 型変換出来ない場合、または引数がnullの場合はfalse
if (other == null) return false;
// 型変換出来た場合は、IEquatable<Account>.Equalsを使って比較する
return Equals(other);
}
}
class Sample {
static void Main()
{
Account[] arr = new Account[] {
new Account(1, "Eve"),
new Account(4, "Dave"),
new Account(2, "Alice"),
new Account(0, "Charlie"),
new Account(3, "Bob"),
};
Account charlie = new Account(0, "Charlie");
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
// objectにキャスト
object obj = charlie;
Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr[3], obj.Equals(arr[3]));
}
}
Imports System
Class Account
Implements IEquatable(Of Account)
Dim id As Integer
Dim name As String
Public Sub New(ByVal id As Integer, ByVal name As String)
MyClass.id = id
MyClass.name = name
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}", id, name)
End Function
' IEquatable(Of Account).Equalsの実装
Public Function Equals(ByVal other As Account) As Boolean Implements IEquatable(Of Account).Equals
If other is Nothing Then Return False
Return MyClass.id = other.id AndAlso MyClass.name = other.name
End Function
' Object.Equalsをオーバーライド
Public Overrides Function Equals(ByVal obj As Object) As Boolean
' Accountに型変換
Dim other As Account = TryCast(obj, Account)
' 型変換出来ない場合、または引数がNothingの場合はFalse
If other Is Nothing Then Return False
' 型変換出来た場合は、IEquatable<Account>.Equalsを使って比較する
Return Equals(other)
End Function
End Class
Class Sample
Shared Sub Main()
Dim arr() As Account = New Account() { _
New Account(1, "Eve"), _
New Account(4, "Dave"), _
New Account(2, "Alice"), _
New Account(0, "Charlie"), _
New Account(3, "Bob") _
}
Dim charlie As New Account(0, "Charlie")
Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr(3), charlie.Equals(arr(3)))
Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie))
' Objectにキャスト
Dim obj As Object = charlie
Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr(3), obj.Equals(arr(3)))
End Sub
End Class
'0:Charlie' Equals '0:Charlie': True IndexOf '0:Charlie': 3 '0:Charlie' Equals '0:Charlie': True
IEqualityComparer, IEqualityComparer<T>
IEqualityComparerインターフェイス(System.Collections名前空間)とIEqualityComparer<T>インターフェイス(System.Collections.Generic名前空間)は、等価性の比較処理を提供するためのインターフェイスです。 IEquatableとIEqualityComparerの関係はIComparableとIComparerの関係に似ていて、IEquatableではインターフェイスを実装する型に比較処理を実装するのに対し、IEqualityComparerでは比較される型とは別に比較処理を実装することが出来ます。
Hashtableはキーの比較にObject.Equals、DictionaryはObject.EqualsまたはIEquatable<T>.Equalsをデフォルトで使用しますが、コンストラクタでキーの比較時に使用するIEqualityComparer・IEqualityComparer<T>を指定することが出来ます これにより、キーとなる型でこれらを実装・オーバーライドする代わりに指定したIEqualityComparer・IEqualityComparer<T>を用いてキーの比較処理を行わせるようにすることが出来ます。
インターフェイスの詳細と実装例は省略します。 次に解説するEqualityComparer<T>クラスを参照してください。
EqualityComparer<T>
EqualityComparer<T>クラス(System.Collections.Generic名前空間)は、IEqualityComparer非ジェネリックインターフェイスとIEqualityComparer<T>ジェネリックインターフェイスを実装する抽象クラスです。 Comparer<T>クラスと同様、IEqualityCompareとIEqualityComparer<T>の二つのインターフェイスを実装するクラスを作成しなくても、このクラスを継承して抽象メソッドであるEqualsメソッドとGetHashCodeメソッドを実装するだけで同じ機能を提供することができるようになります。
EqualityComparer<T>.EqualsメソッドはT型の引数を二つ取り、等しい場合はtrue、そうでない場合はfalseを返すように実装します。 EqualityComparer<T>.GetHashCodeメソッドはT型の引数を一つ取り、引数で指定されたオブジェクトのハッシュ値を返すように実装します。 なお、二つのオブジェクトxとyについて、Equalsメソッドがtrueとなる場合はGetHashCodeメソッドがxとyに対して同じ値を返すように実装しなければなりません。
以下の例では、EqualityComparer<T>を継承したクラスを作成し、Dictionaryでのキーの比較に使用しています。 AccountクラスではIEquatable<T>やEqualsメソッドをオーバーライドしていない点に注目してください。
using System;
using System.Collections.Generic;
class Account {
public int ID;
public string Name;
public Account(int id, string name)
{
this.ID = id;
this.Name = name;
}
public override string ToString()
{
return string.Format("{0}:{1}", ID, Name);
}
}
// AccountクラスのためのEqualityComparer
class AccountComparer : EqualityComparer<Account> {
public override bool Equals(Account x, Account y)
{
// xとyがともにnullの場合は等しいものとする
if (x == null && y == null) return true;
// xとyのどちらかがnullの場合は異なるものとする
else if (x == null || y == null) return false;
// フィールドIDが等しければ二つのAccountは等しいものとする
return x.ID == y.ID;
}
public override int GetHashCode(Account obj)
{
// nullの場合はArgumentNullExceptionをスローする
if (obj == null) throw new ArgumentNullException("obj");
// ハッシュ値にフィールドIDの値を使用する
return obj.ID;
}
}
class Sample {
static void Main()
{
// アカウントとコンタクト先のディクショナリ
Dictionary<Account, Uri> dict = new Dictionary<Account, Uri>(new AccountComparer());
Account alice = new Account(2, "Alice");
Account bob = new Account(0, "Bob");
Account charlie = new Account(1, "Charlie");
dict[alice] = new Uri("mailto:alice@example.net");
dict[bob] = new Uri("mailto:bob@example.net");
dict[charlie] = new Uri("mailto:charlie@example.net");
foreach (KeyValuePair<Account, Uri> pair in dict) {
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
}
Console.WriteLine();
// IDフィールドはBobと同じ、NameフィールドはBobと異なるインスタンスdaveを作成
Account dave = new Account(0, "Dave");
// キーbob, daveに該当する要素が存在するかどうか
Console.WriteLine("ContainsKey '{0}': {1}", bob, dict.ContainsKey(bob));
Console.WriteLine("ContainsKey '{0}': {1}", dave, dict.ContainsKey(dave));
// キーdaveに該当する要素の値を上書き
dict[dave] = new Uri("http://exmaple.net/~dave/");
foreach (KeyValuePair<Account, Uri> pair in dict) {
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
}
}
}
Imports System
Imports System.Collections.Generic
Class Account
Public ID As Integer
Public Name As String
Public Sub New(ByVal id As Integer, ByVal name As String)
MyClass.ID = id
MyClass.Name = name
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}", ID, Name)
End Function
End Class
' AccountクラスのためのEqualityComparer
Class AccountComparer
Inherits EqualityComparer(Of Account)
Public Overrides Function Equals(ByVal x As Account, ByVal y As Account) As Boolean
If x Is Nothing AndAlso y Is Nothing Then
' xとyがともにNothingの場合は等しいものとする
Return True
Else If x Is Nothing OrElse y Is Nothing Then
' xとyのどちらかがnullの場合は異なるものとする
Return False
Else
' フィールドIDが等しければ二つのAccountは等しいものとする
Return x.ID = y.ID
End If
End Function
Public Overrides Function GetHashCode(ByVal obj As Account) As Integer
' Nothingの場合はArgumentNullExceptionをスローする
If obj Is Nothing Then Throw New ArgumentNullException("obj")
' ハッシュ値にフィールドIDの値を使用する
Return obj.ID
End Function
End Class
Class Sample
Shared Sub Main()
' アカウントとコンタクト先のディクショナリ
Dim dict As New Dictionary(Of Account, Uri)(New AccountComparer())
Dim alice As New Account(2, "Alice")
Dim bob As New Account(0, "Bob")
Dim charlie As New Account(1, "Charlie")
dict(alice) = New Uri("mailto:alice@example.net")
dict(bob) = New Uri("mailto:bob@example.net")
dict(charlie) = New Uri("mailto:charlie@example.net")
For Each pair As KeyValuePair(Of Account, Uri) In dict
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value)
Next
Console.WriteLine()
' IDフィールドはBobと同じ、NameフィールドはBobと異なるインスタンスdaveを作成
Dim dave As New Account(0, "Dave")
' キーbob, daveに該当する要素が存在するかどうか
Console.WriteLine("ContainsKey '{0}': {1}", bob, dict.ContainsKey(bob))
Console.WriteLine("ContainsKey '{0}': {1}", dave, dict.ContainsKey(dave))
' キーdaveに該当する要素の値を上書き
dict(dave) = new Uri("http://exmaple.net/~dave/")
For Each pair As KeyValuePair(Of Account, Uri) In dict
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value)
Next
End Sub
End Class
2:Alice => mailto:alice@example.net 0:Bob => mailto:bob@example.net 1:Charlie => mailto:charlie@example.net ContainsKey '0:Bob': True ContainsKey '0:Dave': True 2:Alice => mailto:alice@example.net 0:Bob => http://exmaple.net/~dave/ 1:Charlie => mailto:charlie@example.net
この例ではジェネリックなコレクションであるDictionaryを使っていますが、非ジェネリックなコレクションであるHashtableを使ってもDictionaryと同様に動作します。
EqualityComparer<T>のその他の使用例としてはプロパティ §.プロパティ変更の通知 (INotifyPropertyChanged)の例もご覧ください。
StringComparer
既に解説したStringComparerクラスは、IComparer・IComparer<string>だけでなくIEqualityComparer・IEqualityComparer<string>も実装しています。 以下の例では、StringComparerを使って大文字小文字を無視するDictionaryを作成しています。 StringComparerの詳細については、StringComparerについての個別の解説を参照してください。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
Dictionary<string, Uri> dict = new Dictionary<string, Uri>(StringComparer.CurrentCultureIgnoreCase);
dict["Alice"] = new Uri("mailto:alice@example.net");
dict["Bob"] = new Uri("mailto:bob@example.net");
dict["Charlie"] = new Uri("mailto:charlie@example.net");
foreach (KeyValuePair<string, Uri> pair in dict) {
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
}
Console.WriteLine();
// 各キーに該当する要素の値を上書き
dict["ALICE"] = new Uri("http://example.net/~alice/");
dict["ChArLiE"] = new Uri("mailto:charlie2@mail.example.com");
foreach (KeyValuePair<string, Uri> pair in dict) {
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
}
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim dict As New Dictionary(Of String, Uri)(StringComparer.CurrentCultureIgnoreCase)
dict("Alice") = new Uri("mailto:alice@example.net")
dict("Bob") = new Uri("mailto:bob@example.net")
dict("Charlie") = new Uri("mailto:charlie@example.net")
For Each pair As KeyValuePair(Of String, Uri) In dict
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value)
Next
Console.WriteLine()
' 各キーに該当する要素の値を上書き
dict("ALICE") = new Uri("http://example.net/~alice/")
dict("ChArLiE") = new Uri("mailto:charlie2@mail.example.com")
For Each pair As KeyValuePair(Of String, Uri) In dict
Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value)
Next
End Sub
End Class
Alice => mailto:alice@example.net Bob => mailto:bob@example.net Charlie => mailto:charlie@example.net Alice => http://example.net/~alice/ Bob => mailto:bob@example.net Charlie => mailto:charlie2@mail.example.com