この演習では、C#における等価性のさまざまな性質を学びます。
Object.Equals()==と!=で調べます。これらの型でも使えるEquals()メソッドを使うよりも、こちらのほうが慣用的とされています。Javaを書いてきた人は、文字列を扱うとき、==がC#では値による比較、Javaでは参照による比較になる点に気をつけてください。objectから継承したEquals()メソッドで比較します。等価性のテストで、2つのオブジェクトがまったく同じインスタンスであることを確認したいだけなら、objectの実装に任せておけば十分です。そうでなければ、object.Equals()をオーバーライドする必要があります。Equals()をオーバーライドしなければなりません。Equals()では、単純型のメンバーは==で、参照型のメンバーはEquals()の再帰呼び出しで等価性を調べます。class StatusBar
{
private readonly int width = 200, height = 20;
public override bool Equals(object other)
{
// ... null and type checks and performance optimisations
return width == (other as StatusBar).width && height == (other as StatusBar).height;
}
}
class Window
{
private readonly string title = "Main";
private readonly StatusBar statusBar = new StatusBar();
public override bool Equals(object other)
{
// ... null and type checks and performance optimisations
return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
}
}
object.ReferenceEquals()は、2つのオブジェクトが同じ1つのインスタンスかどうかを調べるために使います。これを使うと意図が明確になり、Equals()や==がオーバーライド・オーバーロードされている場合には欠かせません。var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true
public override bool Equals(object obj)に加えて、IDEは通常、継承が関わる場合に使うオーバーロードprotected bool Equals(FacialFeatures other)を生成します。派生クラスは基底クラスのEquals()を呼び出し、そこに独自のテストを追加できます。==演算子とEquals()メソッドをオーバーロードしていない場合、あるいは参照が等しいことだけが重要なのでなければ、==は使わないでください(operator-overloadingの演習を参照)。structの等価性テストは、structsの演習で扱います。tupleの等価性は、tuplesの演習で扱います。protected bool Equals(T other)
{
return field1 == other.field1 && field2.Equals(other.field2);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((T) obj);
}
SequenceEquals()がありますが、LINQがなければ、両方のコレクションを順にたどって要素を1つずつ比較することになります。==と!=を使う方法については、operator-overloadingの演習を参照してください。Object.GetHashCode()object.GetHashCode()は、32ビットの整数としてハッシュコードを返します。ハッシュコードは、Dictionary<T>やHashSet<T>のような辞書や集合のクラスが、オブジェクトを効率よく格納・取得するために使います。辞書の場合、ハッシュ化の対象はキーです。Equals()をオーバーライドするならGetHashCode()もオーバーライドするもの、という前提があります。Equals()とGetHashCode()の間には、辞書やハッシュ集合、その他ハッシュコードを使うクラスが正しく動作するために守らなければならない関係があります。後からハッシュコードベースのコレクションを追加するかもしれないメンテナーに罠を仕掛けないように、このメソッドを実装することが求められます。Equal()がtrueを返す)なら、その2つのGetHashCode()は同じ値を返さなければなりません。これは逆方向には成り立ちません。対称ではないのです。ハッシュコードに基づいてまず「バケット」に向かい、そこで等価性のテストを使って目的の項目を選び出す、という検索処理を思い浮かべてください。HashCode.Combine()を呼び出し、等価性のテストで使う値(またはその一部)を渡すことです。Combine()に渡す情報が多いほど、ハッシュの実装は効率がよくなる傾向があることを覚えておいてください。public class Assessment
{
private int rating;
private Person boss;
public override int GetHashCode()
{
return HashCode.Combine(rating, boss);
}
}
パフォーマンスを少しでもよくするために、とくにオブジェクトがコレクションに属する場合は、オーバーロードしたメンバーpublic bool Equals(T other)を追加できます。
これにより、参照型では型チェックの手間がいくらか減り、値型ではボックス化の手順を1つ省けます。値型をpublic override bool Equals(object other)の引数としてオブジェクトに変換(ボックス化)する必要がなくなるからです。
クラスにIEquatable<T>インターフェースを追加すると、このオーバーロードを実装する必要があります。IEquatable<T>型のオブジェクトを受け取るルーチン(おそらく、実装クラスに関係なく等価性に関する何か興味深いことをするもの)がコードにない限り、このインターフェースを含めるべき積極的な理由はほかにありません。
IEqualityComparer<T>あるクラスが2通りの方法で一意に識別できるとします。たとえばPersonクラスがSSIDと一意なメールアドレスを持っている場合、.NETには、2つの異なるコレクションで異なるハッシュコードと等価性のテストを使えるようにする仕組みがあります。それぞれが、独自のEquals()とGetHashCode()メソッドを持つ、異なるIEqualityComparer<T>の実装を持てます。SSIDをキーにした辞書と、メールアドレスをキーにした辞書を、それぞれ持つことができます。
IEqualityComparer<T>を使う場合でも、コレクションクラスの外で問題が起きないように、通常は要素のクラスでEquals()とGetHashCode()をオーバーライドしておきます。
IEqualityComparer<T>を使うときの注意点として、privateメソッドなどは等価性のテストに使えません。
ハッシュを使うコレクションが1つだけなら、IEqualityComparer<T>を避けて、等価性とハッシュコードをオブジェクト自身にカプセル化したほうがよいかもしれません。そもそも、オブジェクトとハッシュを使うコレクションの間のような重要な依存関係をコンパイラーが強制できないのは理想的ではありませんが、ハッシュ化と比較のロジックが別の場所にあると、メンテナーがコレクションへの影響を考えずにクラスのメンバーを変更することが、さらに容易になってしまいます。
不注意なプログラマーを困らせるプリミティブの1つが、浮動小数点値の等価性のテストです。これについては、floating-point-numbersの_about.md_で説明しています。
この記事では、等価性と継承に関して下さなければならない判断のいくつかを紹介しています。これらは、あるクラスとその派生クラスのインスタンスを扱い、両者の等価性をテストする必要がある場合にだけ関係してきます。