web-dev-qa-db-ja.com

参照タイプの2つのインスタンスを比較するための「ベストプラクティス」とは何ですか?

私は最近これに遭遇しましたが、今までは等価演算子(==)および/またはEqualsメソッドを使用して、2つの参照タイプに実際に同じdataが含まれているかどうかを確認します(つまり、2つの異なるインスタンスが同じ)。

自動テスト(参照/期待されるデータと返されたデータとの比較)に慣れてきているので、これをさらに使用しています。

いくつかの MSDNのコーディング標準ガイドライン を見ていると、それに対して助言する 記事 に出会いました。今私はなぜ記事がこれを言っているのか(それらは同じではないのでインスタンス)理解しますが、質問には答えません:

  1. 2つの参照タイプを比較する最良の方法は何ですか?
  2. IComparable を実装する必要がありますか? (これは値の型のみに予約する必要があることについても言及しました)。
  3. 知らないインターフェイスはありますか?
  4. 自分で転がせばいいのか!?

どうもありがとう^ _ ^

更新

私はいくつかのドキュメントを誤って読んだようです(長い日でした) Equals をオーバーライドするのが良いでしょう。

参照型を実装する場合、型がPoint、String、BigNumberなどの基本型のように見える場合は、参照型のEqualsメソッドをオーバーライドすることを検討する必要があります。ほとんどの参照型は、equality演算子がEquals。ただし、複素数型など、値のセマンティクスを持つことが意図されている参照型を実装する場合は、等価演算子をオーバーライドする必要があります。

45
Rob Cooper

C#でコーディングしているようです。これは、クラスが実装する必要があるEqualsと呼ばれるメソッドを持っているようです。同じメモリアドレス?」.

here からサンプルコードを取得しました:

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Javaには非常によく似たメカニズムがあります。 equals()メソッドはObjectクラスの一部であり、このタイプの機能が必要な場合は、クラスがオーバーロードします。

'=='のオーバーロードがオブジェクトにとって悪い考えである理由は、通常、「これらは同じポインタである」比較を実行できるようにしたいからです。これらは通常、たとえば、重複が許可されていないリストに要素を挿入する場合に使用されます。この演算子が非標準的な方法でオーバーロードされている場合、フレームワークの一部が機能しない可能性があります。

24
Matt J

.NETで等式を正しく効率的に実装することは、コードの重複なしは困難です。具体的には、値のセマンティクスを持つ参照型(つまり、同値を等価として扱う 不変の型 )の場合、 the System.IEquatable<T> interface を実装し、すべてのさまざまな操作(EqualsGetHashCodeおよび==!=)。

例として、これは価値の平等を実装するクラスです:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

    public override bool Equals(object obj) => Equals(obj as Point);

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

上記のコードで唯一可動な部分は太字の部分です:Equals(Point other)の2行目とGetHashCode()メソッド。他のコードは変更しないでください。

不変の値を表さない参照クラスの場合、演算子==および!=を実装しないでください。代わりに、オブジェクトのアイデンティティを比較するというデフォルトの意味を使用します。

コード意図的には、派生クラス型のオブジェクトにも対応します。基本クラスと派生クラスの等価性が明確に定義されていないため、これは望ましくない場合があります。残念ながら、.NETとコーディングガイドラインはここではあまり明確ではありません。 Equals(object x)およびEquals(SecurableResourcePermission x)willであるため、Resharperが作成し、 別の回答 に投稿したコードは、このような場合に望ましくない動作の影響を受けやすくなります。 _このケースを別の方法で扱います。

この動作を変更するには、上記の厳密に型指定されたEqualsメソッドに追加の型チェックを挿入する必要があります。

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}
26
Konrad Rudolph

以下では、IEquatableを実装するときに行う必要があることを要約し、さまざまなMSDNドキュメントページから正当化を提供しました。


概要

  • 値の等価性をテストする必要がある場合(コレクションでオブジェクトを使用する場合など)、IEquatableインターフェイスを実装し、クラスのObject.Equals、およびGetHashCodeをオーバーライドする必要があります。
  • 参照の等価性をテストする必要がある場合は、operator ==、operator!=および Object.ReferenceEquals を使用する必要があります。
  • ValueTypes および不変の参照タイプの場合、operator ==およびoperator!=のみをオーバーライドする必要があります。

正当化

IEquatable

System.IEquatableインターフェイスは、オブジェクトの2つのインスタンスが等しいかどうかを比較するために使用されます。オブジェクトは、クラスに実装されているロジックに基づいて比較されます。比較の結果、オブジェクトが異なるかどうかを示すブール値が返されます。これは、オブジェクト値の違いを示す整数を返すSystem.IComparableインターフェイスとは対照的です。

IEquatableインターフェイスは、オーバーライドする必要がある2つのメソッドを宣言します。 Equalsメソッドには、実際の比較を実行し、オブジェクト値が等しい場合はtrue、等しくない場合はfalseを返す実装が含まれています。 GetHashCodeメソッドは、異なる値を含む同一のオブジェクトを一意に識別するために使用できる一意のハッシュ値を返す必要があります。使用されるハッシュアルゴリズムのタイプは実装固有です。

IEquatable.Equalsメソッド

  • オブジェクトが配列またはジェネリックコレクションに格納される可能性を処理するには、オブジェクトにIEquatableを実装する必要があります。
  • IEquatableを実装する場合は、Object.Equals(Object)およびGetHashCodeの基本クラス実装もオーバーライドして、IEquatable.Equalsメソッドの動作と一致するようにする必要があります。

Equals()および演算子をオーバーライドするためのガイドライン==(C#プログラミングガイド)

  • x.Equals(x)はtrueを返します。
  • x.Equals(y)はy.Equals(x)と同じ値を返します
  • (x.Equals(y)&& y.Equals(z))がtrueを返す場合、x.Equals(z)はtrueを返します。
  • Xの連続呼び出し。 Equals(y)は、xおよびyによって参照されるオブジェクトが変更されない限り、同じ値を返します。
  • バツ。 Equals(null)はfalseを返します(null不可の値の型のみ。詳細については、「 null許容の型(C#プログラミングガイド) 」を参照してください)。
  • Equalsの新しい実装は例外をスローするべきではありません。
  • Equalsをオーバーライドするクラスは、Object.GetHashCodeもオーバーライドすることをお勧めします。
  • Equals(object)を実装することに加えて、パフォーマンスを向上させるために、どのクラスも独自の型のEquals(type)を実装することをお勧めします。

デフォルトでは、演算子==は、2つの参照が同じオブジェクトを示しているかどうかを判断することにより、参照の等価性をテストします。したがって、参照型は演算子=を実装する必要はありません=この機能を利用するため。型が不変である場合、つまりインスタンスに含まれるデータを変更できない場合、演算子==をオーバーロードして、参照の等価性ではなく値の等価性を比較すると便利です。これは、不変のオブジェクトとして、それらは長さ同じ価値があるからです。 不変でない型の演算子==をオーバーライドすることはお勧めしません。

  • オーバーロードされた演算子==実装は例外をスローするべきではありません。
  • Operator ==をオーバーロードする型は、operator!=もオーバーロードする必要があります。

==演算子(C#リファレンス)

  • 定義済みの値タイプの場合、等価演算子(==)は、そのオペランドの値が等しい場合はtrueを返し、それ以外の場合はfalseを返します。
  • 文字列以外の参照型の場合、2つのオペランドが同じオブジェクトを参照している場合、==はtrueを返します。
  • 文字列型の場合、==は文字列の値を比較します。
  • Operator ==オーバーライド内で==比較を使用してnullをテストする場合は、必ずベースオブジェクトクラス演算子を使用してください。そうしないと、無限再帰が発生し、スタックオーバーフローが発生します。

Object.Equalsメソッド(オブジェクト)

プログラミング言語が演算子のオーバーロードをサポートしていて、特定の型の等価演算子をオーバーロードすることを選択した場合、その型はEqualsメソッドをオーバーライドする必要があります。 Equalsメソッドのこのような実装は、等価演算子と同じ結果を返す必要があります

次のガイドラインは、値タイプを実装するためのものです:

  • Equalsをオーバーライドして、ValueTypeのEqualsのデフォルト実装によって提供されるパフォーマンスよりもパフォーマンスを向上させることを検討してください。
  • Equalsをオーバーライドし、言語が演算子のオーバーロードをサポートしている場合、値の型の等価演算子をオーバーロードする必要があります。

次のガイドラインは、参照タイプを実装するためのものです:

  • タイプのセマンティクスがタイプがいくつかの値を表すという事実に基づいている場合は、参照タイプでEqualsをオーバーライドすることを検討してください。
  • ほとんどの参照型は、Equalsをオーバーライドしても、等価演算子をオーバーロードしてはなりません。ただし、複素数型など、値のセマンティクスを持つことが意図されている参照型を実装する場合は、等価演算子をオーバーライドする必要があります。

追加の問題

15
Zach Burlingame

この記事では、Equalsをオーバーライドするのではなく、(参照型の)等価演算子をオーバーライドしないことを推奨しています。等価チェックが参照チェック以上のものを意味する場合は、オブジェクト(参照または値)内のEqualsをオーバーライドする必要があります。インターフェイスが必要な場合は、 IEquatable を実装することもできます(ジェネリックコレクションで使用)。ただし、IEquatableを実装する場合、IEquatableの備考セクションに次のように記載されているように、equalsもオーバーライドする必要があります。

IEquatable <T>を実装する場合は、Object.Equals(Object)およびGetHashCodeの基本クラスの実装もオーバーライドして、それらの動作がIEquatable <T> .Equalsメソッドの動作と一致するようにする必要があります。 Object.Equals(Object)をオーバーライドする場合、オーバーライドされた実装は、クラスの静的Equals(System.Object、System.Object)メソッドの呼び出しでも呼び出されます。これにより、Equalsメソッドのすべての呼び出しが一貫した結果を返すことが保証されます。

Equalsおよび/または等価演算子を実装する必要があるかどうかに関して:

From Equalsメソッドの実装

ほとんどの参照型は、Equalsをオーバーライドしても、等価演算子をオーバーロードしないでください。

From EqualsおよびEqual演算子(==)を実装するためのガイドライン

等価演算子(==)を実装するときは常にEqualsメソッドをオーバーライドし、それらに同じことを実行させます。

これは、等値演算子を実装するときは常にEqualsをオーバーライドする必要があることを示しています。等しいではありませんは、Equalsをオーバーライドするときに等価演算子をオーバーライドする必要があることを示しています。

3
bdukes

特定の比較を生成する複雑なオブジェクトの場合、IComparableを実装し、Compareメソッドで比較を定義するのが適切な実装です。

たとえば、「Vehicle」オブジェクトがあり、唯一の違いは登録番号である可能性があります。これを使用して比較し、テストで返される期待値が必要な値であることを確認します。

2
Paul Shannon

Resharperが自動的に作成するものを使用する傾向があります。たとえば、これは私の参照型の1つに対してこれを自動作成しました。

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

オーバーライドしたい場合は==とrefチェックを行いますが、Object.ReferenceEquals

1
mattlant

マイクロソフトは調子を変えたようです、または少なくとも等価演算子をオーバーロードしないことに関して矛盾する情報があります。これによると Microsoftの記事 というタイトルの方法:型の値の等価性を定義する:

「==および!=演算子は、クラスがそれらをオーバーロードしていなくてもクラスで使用できます。ただし、デフォルトの動作は、参照等価チェックを実行することです。クラスでは、Equalsメソッドをオーバーロードする場合、 ==および!=演算子ですが、必須ではありません。 "

Eric Lippertによると、彼の answer について質問したところ C#での同等性のための最小限のコード -彼はこう言っています

「ここで遭遇する危険は、デフォルトで等号を参照する==演算子が定義されていることです。オーバーロードされたEqualsメソッドが値の等値を実行し、==が参照の等値を実行し、値が等しい、参照が等しくないものに誤って参照の等価性を使用した場合、これはエラーが発生しやすく、人間のコードレビューでは特定が困難です。

数年前、私は静的分析アルゴリズムを使用してこの状況を統計的に検出しましたが、調査したすべてのコードベースで、コード100万行あたり約2インスタンスの欠陥率が見つかりました。 Equalsをどこかでオーバーライドしたコードベースだけを考えると、欠陥率は明らかにかなり高かったです!

さらに、コストとリスクを考慮してください。 IComparableの実装がすでにある場合、すべての演算子を書くのは、バグがなく、変更されることのない簡単な1行です。これは、これまでに作成する最も安価なコードです。数十の小さなメソッドを作成してテストする固定コストと、値の等価ではなく参照の等価が使用される、見にくいバグを見つけて修正する際の無限のコストのどちらかを選択できれば、どれを選択するかわかります。」

.NET Frameworkは、作成した型で==または!=を使用しません。しかし、危険なのは誰かがそうした場合にどうなるかです。したがって、クラスがサードパーティ向けである場合、私は常に==および!=演算子を提供します。クラスがグループの内部でのみ使用されることを意図している場合でも、おそらく==および!=演算子を実装します。

IComparableが実装されている場合のみ、<、<=、>、および> =演算子を実装します。 IComparableは、型が並べ替えをサポートする必要がある場合にのみ実装する必要があります-並べ替えや、SortedSetのような順序付けされた汎用コンテナーで使用する場合など。

グループまたは会社が==および!=演算子を実装しないようにポリシーを設定している場合は、もちろんそのポリシーに従います。このようなポリシーが実施されている場合は、参照タイプで==および!=演算子が使用されている場合にフラグを立てるQ/Aコード分析ツールを使用してポリシーを適用することをお勧めします。

1
Bob Bryan

上記のすべての回答は多態性を考慮していないため、多くの場合、ベース参照を介して比較した場合でも、派生参照で派生Equalsを使用する必要があります。こちらの質問/ディスカッション/回答をご覧ください- 平等と多態性

0
kofifus

オブジェクトが等しいかどうかを確認するのと同じくらい簡単なものを取得することは、.NETの設計では少し難しいと思います。

Structの場合

1)_IEquatable<T>_を実装します。パフォーマンスが著しく向上します。

2)独自のEqualsを使用しているので、GetHashCodeをオーバーライドし、さまざまな等価性チェックと一貫性を保つために_object.Equals_もオーバーライドします。

3)_==_または_!=_演算子を意図せずに別の構造体と同等に警告するとコンパイラが警告するため、_==_および_!=_演算子のオーバーロードは宗教的に行う必要はありませんが、 Equalsメソッドとの一貫性を保つため。

_public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}
_

クラスの場合

MSから:

ほとんどの参照型は、Equalsをオーバーライドしても、等価演算子をオーバーロードしないでください。

私にとって、_==_は、値の等価性のように感じられ、Equalsメソッドの構文糖のようなものです。 _a == b_の記述は、a.Equals(b)の記述よりもはるかに直感的です。まれに、参照の等価性を確認する必要があります。物理オブジェクトの論理表現を扱う抽象的なレベルでは、これは確認する必要のあるものではありません。 _==_とEqualsのセマンティクスが異なると、実際には混乱する可能性があると思います。そもそも、値の等価性については_==_であり、参照のためのEquals(またはIsSameAsのようなより適切な名前)である必要があります。 ここでは、MSガイドラインを真剣に受け取らないようにしたいと思います。それは、私にとって自然なことではないだけでなく、_==_のオーバーロードが大きな害を及ぼすこともないためです。オーバーライドしないこととは異なります。非ジェネリックEqualsまたはGetHashCode。これは、フレームワークが_==_をどこでも使用しないためです。 _==_および_!=_をオーバーロードしないことで得られる唯一の真の利点は、制御できないフレームワーク全体の設計との整合性です。そして、それは確かに大きなことですとても悲しいことに私はそれに固執します

参照セマンティクス(可変オブジェクト)を使用]

1)EqualsおよびGetHashCodeをオーバーライドします。

2)_IEquatable<T>_の実装は必須ではありませんが、もしあればニースになります。

_public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}
_

値のセマンティクス(不変オブジェクト)付き

これはトリッキーな部分です。気を付けないと簡単にめちゃくちゃになってしまいます。

1)EqualsおよびGetHashCodeをオーバーライドします。

2)_==_および_!=_をオーバーロードして、Equalsに一致させます。 nullに対して機能することを確認してください

2)_IEquatable<T>_の実装は必須ではありませんが、もしあればニースになります。

_public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}
_

クラスを継承できる場合の対処方法については特に注意してください。このような場合、基本クラスオブジェクトが派生クラスオブジェクトと等しいかどうかを判断する必要があります。理想的には、派生クラスのオブジェクトがない場合を使用して等価チェックを行うと、基本クラスインスタンスを派生クラスインスタンスと同じにすることができます。そのような場合は、基本クラスのジェネリックTypeEquals等価をチェックする必要はありません。

一般に、コードが重複しないように注意してください。一般的な抽象基本クラス(_IEqualizable<T>_など)をテンプレートとして作成して、再利用を容易にすることもできましたが、残念ながら、C#では追加のクラスから派生することができません。

0
nawfal