値でオブジェクトを比較する場合-2、またはEqualsメソッドの実装の機能

以前の出版物では、標準の.NETプラットフォームインフラストラクチャを使用してクラスオブジェクトを値で比較できるようにするために、クラスに最小限必要な変更を実装するための一般原則を検討しました。



これらの改善には、重複するメソッドObject.Equals(Object)Object.GetHashCode()が含まれます。



ドキュメントの次の要件を満たすために、 Object.Equals(Object)メソッドの実装機能について詳しく説明します。



x.Equals(y) returns the same value as y.Equals(x).
      
      





前のパブリケーションで作成されたPersonクラスには、次のEquals(Object)メソッドの実装が含まれています。



Person.Equals(オブジェクト)
 public override bool Equals(object obj) { if ((object)this == obj) return true; var other = obj as Person; if ((object)other == null) return false; return EqualsHelper(this, other); }
      
      





現在のオブジェクトと着信オブジェクトの参照の等価性を確認した後、検証の結果が否定の場合、着信オブジェクトは値でオブジェクトを比較できるようにPerson型に変換されます。



ドキュメントに記載されている例に従って、キャストはas演算子を使用し実行さます。 これで正しい結果が得られるかどうかを確認してください。



PersonExクラスを実装し、Personクラスを継承し、個人データにミドルネームプロパティを追加し、それに応じてPerson.Equals(Object)およびPerson.GetHashCode()メソッドをオーバーライドします。



PersonExクラス:



クラスPersonEx
 using System; namespace HelloEquatable { public class PersonEx : Person { public string MiddleName { get; } public PersonEx( string firstName, string middleName, string lastName, DateTime? birthDate ) : base(firstName, lastName, birthDate) { this.MiddleName = NormalizeName(middleName); } public override int GetHashCode() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) { if ((object)this == obj) return true; var other = obj as PersonEx; if ((object)other == null) return false; return EqualsHelper(this, other); } } }
      
      





PersonクラスのオブジェクトでEquals(Object)メソッドを呼び出してPersonExクラスのオブジェクトを渡すと、これらのオブジェクト(person)が同じ名前、姓、生年月日を持っている場合、Equalsメソッドはtrueを返し、そうでない場合はメソッドが返されることに気付くのは簡単です



(Equalsメソッドを実行すると、実行時にPersonExタイプを持つ着信オブジェクトがas演算子を使用してPersonタイプに正常に変換され、Personクラスでのみ使用可能なフィールドの値によってオブジェクトが比較され、対応する関数が返されます結果。)



明らかに、客観的な観点から、これは不正な動作です。



名前、姓、生年月日が一致しても、それが同じ人物であることを意味するわけではありません。 1人はミドルネーム属性を持たず(属性の未定義の値ではなく、属性自体が存在しない)、もう1人はミドルネーム属性を持ちます。

(これらは異なる種類のエンティティです。)



逆に、PersonExクラスのオブジェクトでEquals(Object)メソッドを呼び出してPersonクラスのオブジェクトを渡すと、オブジェクトプロパティの値に関係なく、Equalsメソッドはどのような場合でもfalseを返します。



(Equalsメソッドを実行すると、タイプPersonのランタイムを持つ入力オブジェクトはas演算子を使用してPersonExに正常にキャストされません。キャストの結果はnullになり、メソッドはfalseを返します 。)



ここでは、前のケースとは対照的に、客観的な観点から正しい動作を観察します。



これらの動作は、次のコードを実行することで簡単に検証できます。



コード
 var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person);
      
      





ただし、この出版物の文脈では、客観的な観点からのロジックの正確さよりも、実装されたEquals(Object)の動作とドキュメントの要件との対応に関心があります。



つまり、要件への準拠:



 x.Equals(y) returns the same value as y.Equals(x).
      
      





この要件は満たされていません。



(そして常識的に言えば、Equals(Object)の現在の実装の問題は何でしょうか?

データ型開発者は、オブジェクトの比較方法(x.Equals(y)またはy.Equals(x))に関する情報をクライアントコード(Equalsが明示的に呼び出される場合)とオブジェクトがハッシュセットに配置される場合(ハッシュ )の両方にありません。 カード)および辞書 (セット内/辞書自体)。



この場合、プログラムの動作は非決定的であり、実装の詳細に依存します。)



Equals(Object)メソッドを実装して、期待される動作を実現する方法を検討してください。



CLRを介してCLRでジェフリーリヒターが提案した方法(パートII「設計タイプ」、第5章「プリミティブ型、参照型、および値型」、サブチャプター「オブジェクトの平等と同一性」)は、オブジェクトを値で直接比較することにより、 Object.GetType()メソッドを使用して取得されたオブジェクトのランタイムタイプの等価性がチェックされます(一方向の互換性チェックとas演算子を使用したオブジェクトタイプのキャストの代わりに):



 if (this.GetType() != obj.GetType()) return false;
      
      





このメソッドの使用は明確であることに注意してください。 Typeクラスのインスタンスが等しいかどうかを確認するには、同じオペランドに対して理論的に異なる結果を持つ3つの異なる方法があります。



1. Object.GetType()メソッドのドキュメントによると:



 For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true.
      
      





したがって、 Typeクラスのオブジェクトは、参照による比較によって同等かどうかを確認できます。



 bool isSameType = (object)obj1.GetType() == (object)obj2.GetType();
      
      



または

 bool isSameType = Object.ReferenceEquals(obj1.GetType(), obj2.GetType());
      
      





2. TypeクラスにはEquals(Object)およびEquals(Type)メソッドがあり、その動作は次のように定義されています。
現在のTypeオブジェクトの基本システムタイプが、指定されたObjectの基本システムタイプと同じかどうかを判断します。



戻り値

タイプ:System.Boolean

oの基になるシステムタイプが現在のTypeの基になるシステムタイプと同じである場合はtrue。 それ以外の場合はfalse。 このメソッドは、次の場合にもfalseを返します。

oはnullです。

oは、Typeオブジェクトにキャストまたは変換できません。



備考

このメソッドはObject.Equalsをオーバーライドします。 oをType型のオブジェクトにキャストし、Type.Equals(Type)メソッドを呼び出します。
そして
現在のTypeの基になるシステムタイプが、指定したTypeの基になるシステムタイプと同じかどうかを判断します。



戻り値

タイプ:System.Boolean

oの基になるシステムタイプが現在のTypeの基になるシステムタイプと同じである場合はtrue。 それ以外の場合はfalse。


内部的に、これらのメソッドは次のように実装されます。



 public override bool Equals(Object o) { if (o == null) return false; return Equals(o as Type); }
      
      





そして



 public virtual bool Equals(Type o) { if ((object)o == null) return false; return (Object.ReferenceEquals(this.UnderlyingSystemType, o.UnderlyingSystemType)); }
      
      





ご覧のとおり、一般的なケースでTypeクラスのオブジェクトに対して両方のEqualsメソッドを実行した結果は、参照によるオブジェクトの比較とは異なる場合があります。 Equalsメソッドを使用する場合、 Typeクラスのオブジェクトは参照によって比較されるのではなく、同じクラスに属するUnderlyingSystemTypeプロパティが比較されます。



ただし、 Type.Equals(Object)クラスのEqualsメソッドの説明から、Typeクラスのオブジェクトを直接比較することは意図されていないようです。



注:

Type.Equals(オブジェクト)メソッドの場合、要件に準拠していないという問題( as演算子を使用した結果)

 x.Equals(y) returns the same value as y.Equals(x).
      
      



Typeクラスの子孫でメソッドが誤ってオーバーラップされない限り、発生しません。

この潜在的な問題を防ぐために、開発者がメソッドを封印済みとして宣言することが適切な場合があります。



3. .NET Framework 4.0以降のTypeクラスには、オーバーロードされた==または!=演算子があり、その動作は実装の詳細を説明せずに簡単な方法で説明されています。

2つのTypeオブジェクトが等しいかどうかを示します。



戻り値

タイプ:System.Boolean

左が右に等しい場合はtrue。 それ以外の場合はfalse。
そして
2つのTypeオブジェクトが等しくないかどうかを示します。



戻り値

タイプ:System.Boolean

左が右と等しくない場合はtrue。 それ以外の場合はfalse。
また、ソースコードの調査では、演算子の内部ロジックを明確にするために、実装の詳細に関する情報は提供されません。



 public static extern bool operator ==(Type left, Type right);
      
      



 public static extern bool operator !=(Type left, Type right);
      
      





Typeクラスのオブジェクトを比較するために文書化された3つのメソッドの分析に基づくと、オブジェクトを比較する最も正しい方法は、「==」と「!=」演算子を使用することです。

プロジェクトのターゲットプラットフォームに応じて、ソースコードは、参照による比較(最初のオプションと同じ)またはオーバーロード演算子==および!=を使用してコンパイルされます。



それに応じてPersonクラスとPersonExクラスを実装します。



クラスPerson(新しいEqualsメソッドを使用)
 using System; namespace HelloEquatable { public class Person { protected static string NormalizeName(string name) => name?.Trim() ?? string.Empty; protected static DateTime? NormalizeDate(DateTime? date) => date?.Date; public string FirstName { get; } public string LastName { get; } public DateTime? BirthDate { get; } public Person(string firstName, string lastName, DateTime? birthDate) { this.FirstName = NormalizeName(firstName); this.LastName = NormalizeName(lastName); this.BirthDate = NormalizeDate(birthDate); } public override int GetHashCode() => this.FirstName.GetHashCode() ^ this.LastName.GetHashCode() ^ this.BirthDate.GetHashCode(); protected static bool EqualsHelper(Person first, Person second) => first.BirthDate == second.BirthDate && first.FirstName == second.FirstName && first.LastName == second.LastName; public override bool Equals(object obj) { if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (Person)obj); } } }
      
      





クラスPersonEx(新しいEqualsメソッドを使用)
 using System; namespace HelloEquatable { public class PersonEx : Person { public string MiddleName { get; } public PersonEx( string firstName, string middleName, string lastName, DateTime? birthDate ) : base(firstName, lastName, birthDate) { this.MiddleName = NormalizeName(middleName); } public override int GetHashCode() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) { if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (PersonEx)obj); } } }
      
      





これで、 Equals(Object)メソッドを実装するための次の要件が満たされます。



 x.Equals(y) returns the same value as y.Equals(x).
      
      





コード実行により簡単に確認できます:



コード
 var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person);
      
      





Equals(Object)メソッドの実装に関する注意:



  1. 最初に、現在のオブジェクトと着信オブジェクトを指すリンクが等しいかどうかがチェックされ、リンクが一致する場合はtrueが返されます。
  2. 次に、着信オブジェクトへのnull参照がチェックされ、テスト結果が肯定の場合、 falseが返されます。
  3. 次に、現在のオブジェクトと着信オブジェクトのタイプのIDがチェックされ、チェックの結果が否定の場合はfalseが返されます。
  4. 最後の段階で、入力オブジェクトはこのクラスの型に変換され、オブジェクトは値によって直接比較されます。


したがって、 Equals(Object)メソッドの予想される動作を実装する最良の方法を見つけました。



デザートについては、標準ライブラリのEquals(Object)の正しい実装を確認してください。



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



2つのUriインスタンスが等しいかどうかを比較します。



構文

public override bool Equals(オブジェクト比較)



パラメータ

被比較者

タイプ:System.Object

現在のインスタンスと比較するためのUriインスタンスまたはURI識別子。



戻り値

タイプ:System.Boolean

2つのインスタンスが同じURIを表す場合にtrueになるブール値。 それ以外の場合はfalse。


Uri.Equals(オブジェクト)
 public override bool Equals(object comparand) { if ((object)comparand == null) { return false; } if ((object)this == (object)comparand) { return true; } Uri obj = comparand as Uri; // // we allow comparisons of Uri and String objects only. If a string // is passed, convert to Uri. This is inefficient, but allows us to // canonicalize the comparand, making comparison possible // if ((object)obj == null) { string s = comparand as string; if ((object)s == null) return false; if (!TryCreate(s, UriKind.RelativeOrAbsolute, out obj)) return false; } // method code ... }
      
      





Equals(Object)メソッドの実装に関する次の要件が満たされていないと仮定することは論理的です。



 x.Equals(y) returns the same value as y.Equals(x).
      
      





なぜなら 一方、 StringクラスとString.Equals(Object)メソッドは、 Uriクラスの存在を「認識」しません。



これは実際にコードを実行することで簡単に確認できます:



コード
 const string uriString = "https://www.habrahabr.ru"; Uri uri = new Uri(uriString); bool isSameUri = uri.Equals(uriString); bool isSameUri2 = uriString.Equals(uri);
      
      





続編では、 IEquatable(Of T)インターフェイスと型固有のIEquatable(Of T).Equals(T)メソッドの実装を検討し、 等値演算子と不等値演算子をオーバーロードして、オブジェクトを値で比較し、最もコンパクトで一貫性のある効率的な方法を見つけますすべての種類のチェックを値で分類します。




All Articles