オブジェクトの比較
比較ルールの定義
クラスを自作した場合、そのままではインスタンス同士を比較することはできません。
これはそのクラスには比較のための方法が定義されていないためです。
(等値の比較のみは限定的ながら可能です。後述)
配列のSortメソッドやListのSortメソッドを使用するためには、オブジェクト同士を比較して「並べ替えの順序」を何らかの方法で決定できなければなりません。
数値型ならば単純に数値の大小、文字列型ならばABC順などですが、自作クラスの場合はその順序決定のルールを自分で定義する必要があります。
配列のSortメソッドやListのSortメソッドでも説明していますが、これらのメソッドは並べ替え順序を決定するメソッドを引数に指定する方法もあります。
このページは比較用のメソッドによる比較の説明です。
演算子による比較を行う場合は演算子のオーバーロードの項を参照してください。
等値演算子による比較
比較処理の説明の前に、等値演算子(==
)による比較を行った場合の挙動を説明しておきます。
(演算子のオーバーロードをしていない場合)
ユーザー定義クラス(自作クラス)に対する等値演算子は、同一のインスタンスを比較した場合(インスタンスの参照先が同じである場合)に真となります。
メンバが保存する値が同じでも異なるインスタンスの場合は偽となります。
class Test
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
}
static Test F(Test t)
{
//引数に受け取ったインスタンスをそのまま返す
return t;
}
static void Main(string[] args)
{
Test t1 = new Test(1, "abc");
Test t2 = new Test(1, "abc");
Console.WriteLine(t1 == F(t1));
Console.WriteLine(t1 == t2);
Console.ReadLine();
}
True False
参照型変数のコピーは参照情報のコピーなので、関数の引数や戻り値等でインスタンスが受け渡しされても参照情報は変わりません。
自作の構造体の場合は、演算子オーバーロードをしなければ等値演算子は使用できません。
IComparable
ユーザー定義クラスを比較可能にするにはIComparableインターフェイスを継承する方法があります。
IComparableインターフェイスはint CompareTo(object obj)
というメソッドを持っているので、これを派生クラスで実装します。
インターフェイスについてはインターフェイスの項を参照してください。
//IComparableの継承
class Test : IComparable
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
//CompareToメソッドを実装
public int CompareTo(object obj)
{
Test t = obj as Test;
if (t == null)
return 1;
int n = Num.CompareTo(t.Num);
if (n != 0)
return n;
if (Str == null)
return -1;
return Str.CompareTo(t.Str);
}
}
static void Main(string[] args)
{
List<Test> tests = new List<Test>()
{
new Test(1, "zzz"),
new Test(2, "bbb"),
new Test(3, "aaa"),
new Test(1, "bbb"),
new Test(3, "ccc"),
};
//Sortメソッドが使用可能
tests.Sort();
foreach (var t in tests)
{
Console.WriteLine("{0} {1}", t.Num, t.Str);
}
Console.ReadLine();
}
1 bbb 1 zzz 2 bbb 3 aaa 3 ccc
CompareTo
メソッドは、自分自身と引数に指定した値とを比較して、自分自身が小さい場合は0未満を、自分自身のほうが大きければ0より大きい値を返します。
同じ値であった場合には0を返します。
0未満を返した場合、自分自身は引数の値よりも並び順が前になることを意味します。
int型やstring型などはそれ自身がCompareTo
メソッドを持っているので、自作クラスのCompareTo
メソッド内でもこれを利用します。
(これを使用せず独自の比較ルールを定義することも可能です)
サンプルコードのCompareTo
メソッドでは、まずフィールドNum
の値を比較対象にし、順序が同じだった場合にはフィールドStr
で比較を行います。
フィールドStr
も同じだった場合には同じ順序と判定されます。
int型などの組み込み型は内部的には構造体として実装されており、この構造体はIComparable
を継承して作られています。
つまりCompareTo
メソッドも定義されています。
配列、ListクラスのSort
メソッドは、対象がIComparable派生クラスである場合はCompareTo
メソッドを呼び出して並べ替えを実行します。
IComparable<T>
IComparableインターフェイスにはジェネリック版も存在します。
IComparableジェネリックインターフェイスはint CompareTo(T other)
というメソッドを持っているので、これを派生クラスで定義します。
T
には自作のクラス型を指定します。
引数の型が異なる以外はほぼ非ジェネリック版と同じです。
非ジェネリック版、ジェネリック版はどちらか一方の継承で構いません。
//IComparable<T>の継承
class Test : IComparable<Test>
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
//CompareToメソッドを実装
public int CompareTo(Test t)
{
if (t == null)
return 1;
int n = Num.CompareTo(t.Num);
if (n != 0)
return n;
if (Str == null)
return -1;
return Str.CompareTo(t.Str);
}
}
static void Main(string[] args)
{
//省略
}
IComparer<T>
IComparableインターフェイスは自分自身でクラスを定義する場合には有効な方法ですが、組み込み型や外部ライブラリなどで定義されているデータ型はコードを変更できないためIComparableインターフェイスを実装できない場合があります。
その場合は比較のためのコードをIComparerインターフェイスを継承した別のクラスで実装します。
IComparerにもジェネリック版と非ジェネリック版がありますが、ほとんどの場合でジェネリック版の実装で良いでしょう。
IComparer<T>インターフェイス(IComparerのジェネリックインターフェイス)はint Compare(T x, T y)
というメソッドを持っているので、これを実装します。
T
には比較したいクラスを指定します。
引数x
が自分自身で、引数y
が比較するインスタンスです。
処理自体はIComparableインターフェイスのCompareTo
メソッドとほとんど同じです。
//こんなクラスが外部ライブラリに存在するとする
//このコードは変更できない
class Test
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
}
//Testクラスを比較するための
//クラスを定義
class TestComparer : IComparer<Test>
{
public int Compare(Test t1, Test t2)
{
if(t1 == null)
{
if(t2 == null)
return 0;
return -1;
}
if(t2 == null)
return 1;
int n = t1.Num.CompareTo(t2.Num);
if (n != 0)
return n;
if (t1.Str == null)
return -1;
return t1.Str.CompareTo(t2.Str);
}
}
static void Main(string[] args)
{
List<Test> tests = new List<Test>()
{
new Test(1, "zzz"),
new Test(2, "bbb"),
new Test(3, "aaa"),
new Test(1, "bbb"),
new Test(3, "ccc"),
};
//TestComparerのインスタンスを引数に指定
tests.Sort(new TestComparer());
foreach (var t in tests)
{
Console.WriteLine("{0} {1} ", t.Num, t.Str);
}
Console.ReadLine();
}
文字列の比較を行う場合は.NET標準のStringComparerの使用をおすすめします。
Equals
C#ではすべてのデータ型はobject型から派生しており、何も継承しないクラスを定義しても暗黙的にobject型を継承しています。
object型にはEquals
という比較のためのメソッドが定義されているため、何も定義していない自作のクラスでもEquals
メソッドは使用できます。
Equals
メソッドは等値演算子による比較と同じく、オブジェクト同士が等しい場合に真を返します。
データ型が値型であれば特に問題はありませんが、クラスなどの参照型の場合は「同じ参照元」を指している場合に等しいと判定されます。
そのままでは都合が悪い場合、Equals
メソッドをオーバーライドして動作を変更することができます。
class Test
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
//Equalsメソッドのオーバーライド
public override bool Equals(object obj)
{
Test t = obj as Test;
if (t == null)
return false;
return Num == t.Num && Str == t.Str;
}
//Equalsメソッドをオーバーライドする場合
//GetHashCodeメソッドもオーバーライドする
public override int GetHashCode()
{
if (Str == null)
return Num;
return Num ^ Str.GetHashCode();
}
}
Equals
メソッドはpublic virtual bool Equals(object obj)
という定義なので、これをオーバーライドします。
派生元となるobject型はインターフェイス型ではないため、override
キーワードが必要です。
サンプルコードではフィールドNum
とStr
の両方の値が等しい場合に真を返しています。
GetHashCodeメソッドのオーバーライド
Equals
メソッドをオーバーライドする場合、GetHashCode
というメソッドもオーバーライドします。
GetHashCode
メソッドはDictionaryクラスのキーなどで使用されるメソッドで、Equals
メソッドの動作を変更すると上手く動作しなくなる場合があるので一緒にオーバーライドしておきます。
GetHashCode
メソッドはEquals
メソッドが真を返すインスタンスからは常に同じ値を返すというものです。
この「値」をハッシュコード(ハッシュ値)といいます。
実装の方法はいくつかあるようですが、広く使われているのは比較対象となるフィールド(またはプロパティ)のGetHashCode
の値を^演算子(XOR)でビット演算した値を返す、というものです。
ちなみにint型などの整数型は、その数値自身で等値性が判定できるのでGetHashCode
メソッドを使用しなくても問題ありません。
(整数型のGetHashCode
メソッドは、その値自信を返す実装もあれば別の値を返す実装もあります)
ハッシュコードは、同じ値を持つオブジェクト同士では必ず一致しなければなりませんが、異なる値を持つオブジェクト同士ではハッシュコードが一致してはならない、というものではありません。
例えば整数値を100で剰余した値をハッシュコードとする場合、「1」と「101」が同じハッシュコード(1)になりますが、このような実装があっても構いません。
そのため、ハッシュコードはオブジェクトの同一性を判定するためには使えないことに注意してください。
またその実装や値そのものに依存するようなコードはバグの原因となります。
ReferenceEqualsメソッド
Equals
メソッドと似ているものにobject.ReferenceEquals
という静的メソッドがあります。
このメソッドは「オブジェクトの参照元が同じか否か」を判定します。
つまり同じ値というだけでなく、オブジェクトそのものが同一である場合に真が返されます。
この動作はEquals
メソッドの規定の動作と同じですが、Equals
メソッドはobject型内では仮想関数(virtual
)で定義されているので、派生クラスでオーバーライドされる可能性があります。
Equals
メソッドには静的メソッド版(object.Equals
)がありますが、これも内部的にインスタンスメソッドのEquals
メソッドを呼び出しているため、派生クラスで動作が変更される可能性があります。
ReferenceEquals
メソッドはEquals
メソッドのオーラーライドに影響を受けず、このメソッド自身のオーバーライドもできないので、常にオブジェクトの同一性を判定することができます。
class Test
{
public int Num;
public override bool Equals(object other)
{
Test t = other as Test;
if(t == null)
return false;
return Num == t.Num;
}
public override int GetHashCode() { return Num; }
}
static void Main(string[] args)
{
Console.WriteLine(
"object.Equals(test1, test1) -> {0}",
object.Equals(test1, test1));
Console.WriteLine(
"object.Equals(test1, test2) -> {0}",
object.Equals(test1, test2));
Console.WriteLine(
"object.ReferenceEquals(test1, test1) -> {0}",
object.ReferenceEquals(test1, test1));
Console.WriteLine(
"object.ReferenceEquals(test1, test2) -> {0}",
object.ReferenceEquals(test1, test2));
Console.ReadLine();
}
object.Equals(test1, test1) -> True object.Equals(test1, test2) -> True object.ReferenceEquals(test1, test1) -> True object.ReferenceEquals(test1, test2) -> False
IEqualityComparer
Equalsメソッドのオーバーライドは自作メソッドの場合に可能ですが、組み込み型や外部ライブラリなどで定義されているデータ型の場合はコードが変更できないためEquals
メソッドをオーバーライドできません。
Equals
メソッドと同等の定義が必要な場合はIEqualityComparerインターフェイスを継承したクラスを別に用意します。
//こんなクラスが外部ライブラリに存在するとする
//このコードは変更できない
class Test
{
public int Num;
public string Str;
public Test(int n, string s)
{
Num = n;
Str = s;
}
}
//Testクラスを比較するための
//クラスを定義
class TestComparer : IEqualityComparer<Test>
{
public bool Equals(Test x, Test y)
{
if(x == null)
{
if(y == null)
return true;
return false;
}
if(y == null)
return false;
return x.Num == y.Num && x.Str == y.Str;
}
public int GetHashCode(Test t)
{
if (t == null)
return 0;
if (t.Str == null)
return t.num;
return t.Num ^ t.Str.GetHashCode();
}
}
static void Main(string[] args)
{
List<Test> tests = new List<Test>()
{
new Test(1, "aaa"),
new Test(2, "bbb"),
new Test(1, "aaa")
};
//TestComparerのインスタンスを引数に指定
IEnumerable<Test> distinct =
tests.Distinct(new TestComparer());
foreach (var x in distinct)
Console.WriteLine("{0} {1}", x.Num, x.Str);
Console.ReadLine();
}
IEqualityComparerインターフェイスではbool Equals(T x, T y)
とint GetHashCode(T obj)
の二つが定義されているので、これらを実装します。
T
には比較したいクラスを指定します。
引数x
が自分自身、引数y
が比較するインスタンスです。
コードの中身はEquals
メソッドのオーバーライドの場合とほとんど同じです。
サンプルコードではフィールドNum
とStr
の両方の値が等しい場合に真を返しています。
例えばLINQのDistinctメソッドは、要素が参照型の場合に「Equalsメソッドのオーバーライド」か「IEqualityComparerインターフェイス」のどちらかの方法を用いる必要があります。
文字列の比較を行う場合は.NET標準のStringComparerの使用をおすすめします。
StringComparer
上記は比較のルールを自作する場合の話でしたが、.NETにはStringComparerという文字列比較のためのクラスが用意されています。
StringComparerはIComparerとIEqualityComparerを継承した抽象クラスで、以下のプロパティが存在します。
(Microsoft Docs)
CurrentCulture | 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する |
CurrentCultureIgnoreCase | 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する |
InvariantCulture | インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する |
InvariantCultureIgnoreCase | インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する |
Ordinal | 大文字と小文字を区別して序数の文字列比較を実行する |
OrdinalIgnoreCase | 大文字と小文字を区別せずに序数の文字列比較を実行する |
カルチャというのは、国や地域、言語によって異なるものをまとめた情報のことです。
(Culture=文化)
例えば日付の表記は日本とアメリカでは書式が異なります。
C#プログラムには「現在のカルチャ」が設定されており、それに基づいて書式が決定されます。
インバリアントカルチャというのは特定の国、地域、言語に依存しないカルチャのことです。
ベースは北米圏のカルチャとなっています。
特定のカルチャに依存しないので、多言語化を前提としたプログラムで共通して使用したいデータなどに利用されています。
序数の文字列比較とは、文字を内部的に定義されている順序通りに比較を行います。
例えば「a」と「A」では内部的には「A」のほうが先に定義されており、並び順が先になります。
(文字コード。Aは65番目、aは97番目に定義されている)
文字列の意味などは考慮しないので他のふたつより若干高速に動作します。
ほとんどの場合でOrdinal(IgnoreCase)かCurrentCulture(IgnoreCase)が使用されます。
「IgnoreCase」の有無は大文字小文字を無視するかしないかの違いです。
(Ignore=無視、Case=大文字小文字(UppweCase、LowerCase))
なお、これらのプロパティはStringComparerのインスタンスを返すので、newキーワードは必要ありません。
(というより抽象クラスなのでnewは使えません)
List<string> strs = new List<string>()
{
"aaa", "bbb", "Aaa"
};
//大文字小文字を無視
IEnumerable<string> distinct =
strs.Distinct(StringComparer.OrdinalIgnoreCase);