等価

等価性 の C#

5個の演習

等価性について

この演習では、C#における等価性のさまざまな性質を学びます。

Object.Equals()

演習で扱うトピック

  • 単純型(文字列やプリミティブ)の等価性は、通常==と!=で調べます。これらの型でも使えるEquals()メソッドを使うよりも、こちらのほうが慣用的とされています。Javaを書いてきた人は、文字列を扱うとき、==がC#では値による比較、Javaでは参照による比較になる点に気をつけてください。
  • 参照型(クラスのインスタンス)は、objectから継承したEquals()メソッドで比較します。等価性のテストで、2つのオブジェクトがまったく同じインスタンスであることを確認したいだけなら、objectの実装に任せておけば十分です。そうでなければ、object.Equals()をオーバーライドする必要があります。
  • 自分のクラスのインスタンスがすべて1か所で作られるとわかっている場合、たとえばゲームやシミュレーションのキャラクターなどなら、参照の等価性で十分です。しかし、同じ実体を表すインスタンスが複数作られることもよくあります(たとえば、データベースから、ユーザーの入力によって、Webリクエストを経由してなど)。この場合は、その実体を一意に識別する値を比較して等価性を判定する必要があります。そのため、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の演習で扱います。
  • 多くのエンジニアは、等価性に関する細かな点をすべて面倒見てくれるIDEに、等価性メソッドの実装を任せています。たとえば、JetBrainsのRIDER(v 2020.1)は、クラスに対して次の等価性メソッドを生成します。
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);
}
  • IDEが生成したコードを自分で改良しようとするときは注意してください。生成されたコードは一般に、nullや型の間違ったオブジェクトに対して堅牢で、かなりよく最適化されています。
  • デリゲートの等価性テストは、この演習では特に扱いません。
  • 配列やほとんどのコレクションには、組み込みの等価性テストがありません。LINQ(後の演習で扱います)にはSequenceEquals()がありますが、LINQがなければ、両方のコレクションを順にたどって要素を1つずつ比較することになります。
  • 自分のクラスで==と!=を使う方法については、operator-overloadingの演習を参照してください。

Object.GetHashCode()

  • object.GetHashCode()は、32ビットの整数としてハッシュコードを返します。ハッシュコードは、Dictionary<T>やHashSet<T>のような辞書や集合のクラスが、オブジェクトを効率よく格納・取得するために使います。辞書の場合、ハッシュ化の対象はキーです。
  • C#のエンジニアの間では、Equals()をオーバーライドするならGetHashCode()もオーバーライドするもの、という前提があります。Equals()とGetHashCode()の間には、辞書やハッシュ集合、その他ハッシュコードを使うクラスが正しく動作するために守らなければならない関係があります。後からハッシュコードベースのコレクションを追加するかもしれないメンテナーに罠を仕掛けないように、このメソッドを実装することが求められます。
  • ハッシュコードと等価性の関係はこうです。2つのオブジェクトが等しい(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_で説明しています。

等価性と継承

この記事では、等価性と継承に関して下さなければならない判断のいくつかを紹介しています。これらは、あるクラスとその派生クラスのインスタンスを扱い、両者の等価性をテストする必要がある場合にだけ関係してきます。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます

等価性を学習する

練習はロックされています

等価性を練習するには、あと4個の演習のロックを解除してください